blob: d0e4148d4e1202874a868890e57e5fa1793ac921 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2002, 2020 Innoopract Informationssysteme GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Innoopract Informationssysteme GmbH - initial API and implementation
* EclipseSource - ongoing development
******************************************************************************/
package org.eclipse.swt.browser;
import static org.eclipse.rap.rwt.internal.service.ContextProvider.getApplicationContext;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.rap.rwt.internal.lifecycle.ProcessActionRunner;
import org.eclipse.rap.rwt.internal.lifecycle.SimpleLifeCycle;
import org.eclipse.rap.rwt.internal.lifecycle.WidgetLCA;
import org.eclipse.rap.rwt.internal.lifecycle.WidgetUtil;
import org.eclipse.rap.rwt.internal.service.ContextProvider;
import org.eclipse.rap.rwt.internal.service.ServiceStore;
import org.eclipse.rap.rwt.internal.util.ParamCheck;
import org.eclipse.rap.rwt.widgets.BrowserCallback;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.internal.SWTEventListener;
import org.eclipse.swt.internal.browser.browserkit.BrowserLCA;
import org.eclipse.swt.internal.events.EventTypes;
import org.eclipse.swt.internal.widgets.IBrowserAdapter;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.TypedListener;
/**
* Instances of this class implement the browser user interface
* metaphor. It allows the user to visualize and navigate through
* HTML documents.
* <p>
* Note that although this class is a subclass of <code>Composite</code>,
* it does not make sense to set a layout on it.
* </p><p>
* IMPORTANT: This class is <em>not</em> intended to be subclassed.
* </p>
*
* @since 1.0
*
* <hr/>
* <p>Currently implemented</p>
* <ul><li>text and url property</li></ul>
* <p>The enabled property in not (yet) evaluated.</p>
* <p>Focus events are not yet implemented</p>
*
*/
// TODO [rh] implement refresh method
// TODO [rh] bring focus events to work
public class Browser extends Composite {
private static final String FUNCTIONS_TO_CREATE
= Browser.class.getName() + "#functionsToCreate.";
private static final String FUNCTIONS_TO_DESTROY
= Browser.class.getName() + "#functionsToDestroy.";
static final String ABOUT_BLANK = "about:blank";
private String url;
private String html;
private boolean urlChanged;
private String executeScript;
private Boolean executeResult;
private boolean executePending;
private Object evaluateResult;
private BrowserCallback browserCallback;
private transient IBrowserAdapter browserAdapter;
private final List<BrowserFunction> functions;
/**
* Constructs a new instance of this class given its parent
* and a style value describing its behavior and appearance.
* <p>
* The style value is either one of the style constants defined in
* class <code>SWT</code> which is applicable to instances of this
* class, or must be built by <em>bitwise OR</em>'ing together
* (that is, using the <code>int</code> "|" operator) two or more
* of those <code>SWT</code> style constants. The class description
* lists the style constants that are applicable to the class.
* Style bits are also inherited from superclasses.
* </p>
*
* @param parent a widget which will be the parent of the new instance (cannot be null)
* @param style the style of widget to construct
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
* </ul>
* @exception SWTError <ul>
* <li>ERROR_NO_HANDLES if a handle could not be obtained for browser creation</li>
* </ul>
*
* @see org.eclipse.swt.widgets.Widget#getStyle
*/
public Browser( Composite parent, int style ) {
super( parent, checkStyle( style ) );
html = "";
url = "";
functions = new ArrayList<>();
addDisposeListener( new BrowserDisposeListener() );
}
/**
* Loads a URL.
*
* @param url the URL to be loaded
*
* @return true if the operation was successful and false otherwise.
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the url is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @see #getUrl
*/
public boolean setUrl( String url ) {
checkWidget();
if( url == null ) {
SWT.error( SWT.ERROR_NULL_ARGUMENT );
}
boolean result = sendLocationChangingEvent( url );
if( result ) {
this.url = url;
urlChanged = true;
html = "";
sendLocationChangedEvent( url );
sendProgressChangedEvent();
}
return result;
}
/**
* Returns the current URL.
*
* @return the current URL or an empty <code>String</code> if there is no current URL
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @see #setUrl
*/
public String getUrl() {
checkWidget();
return url;
}
/**
* Renders HTML.
*
* <p>
* The html parameter is Unicode encoded since it is a java <code>String</code>.
* As a result, the HTML meta tag charset should not be set. The charset is implied
* by the <code>String</code> itself.
*
* @param html the HTML content to be rendered
*
* @return true if the operation was successful and false otherwise.
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the html is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @see #setUrl
*/
public boolean setText( String html ) {
checkWidget();
if( html == null ) {
SWT.error( SWT.ERROR_NULL_ARGUMENT );
}
boolean result = sendLocationChangingEvent( ABOUT_BLANK );
if( result ) {
this.html = html;
url = "";
urlChanged = true;
sendLocationChangedEvent( ABOUT_BLANK );
sendProgressChangedEvent();
}
return result;
}
/**
* Execute the specified script.
*
* <p>Execute a script containing javascript commands in the context of the
* current document.</p>
*
* <!-- Begin RAP specific -->
* <p><strong>RAP Note:</strong> Care should be taken when using this method.
* The given <code>script</code> is executed in an <code>IFRAME</code>
* inside the document that represents the client-side application.
* Since the execution context of an <code>IFRAME</code> is not fully
* isolated from the surrounding document it may break the client-side
* application.</p>
* <p>This method is not supported when running the application in JEE_COMPATIBILITY mode.
* Use <code>evaluate(String, BrowserCallBack)</code> instead.</p>
* <p>This method will throw an IllegalStateException if called while another script is still
* pending or executed. This can happen if called within a BrowserFunction, or if an SWT event
* is pending to be executed. (E.g. clicking a Button twice very fast.)
* </p>
* <!-- End RAP specific -->
*
* @param script the script with javascript commands
*
* @return <code>true</code> if the operation was successful and
* <code>false</code> otherwise
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the script is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @exception UnsupportedOperationException when running the application in JEE_COMPATIBILITY mode
* @exception IllegalStateException when another script is already being executed.
*
* @see org.eclipse.rap.rwt.application.Application.OperationMode
*
* @since 1.1
*/
public boolean execute( String script ) {
checkOperationMode();
checkWidget();
if( script == null ) {
SWT.error( SWT.ERROR_NULL_ARGUMENT );
}
if( executeScript != null ) {
throw new IllegalStateException( "Another script is already pending" );
}
executeScript = script;
executeResult = null;
while( executeResult == null ) {
Display display = getDisplay();
if( !display.readAndDispatch() ) {
display.sleep();
}
}
executeScript = null;
executePending = false;
return executeResult.booleanValue();
}
/**
* Returns the result, if any, of executing the specified script.
* <p>
* Evaluates a script containing javascript commands in the context of
* the current document. If document-defined functions or properties
* are accessed by the script then this method should not be invoked
* until the document has finished loading (<code>ProgressListener.completed()</code>
* gives notification of this).
* </p><p>
* If the script returns a value with a supported type then a java
* representation of the value is returned. The supported
* javascript -> java mappings are:
* <ul>
* <li>javascript null or undefined -> <code>null</code></li>
* <li>javascript number -> <code>java.lang.Double</code></li>
* <li>javascript string -> <code>java.lang.String</code></li>
* <li>javascript boolean -> <code>java.lang.Boolean</code></li>
* <li>javascript array whose elements are all of supported types -> <code>java.lang.Object[]</code></li>
* </ul>
*
* An <code>SWTException</code> is thrown if the return value has an
* unsupported type, or if evaluating the script causes a javascript
* error to be thrown.
*
* <!-- Begin RAP specific -->
* <p><strong>RAP Note:</strong> Care should be taken when using this method.
* The given <code>script</code> is executed in an <code>IFRAME</code>
* inside the document that represents the client-side application.
* Since the execution context of an <code>IFRAME</code> is not fully
* isolated from the surrounding document it may break the client-side
* application.</p>
* <p>This method is not supported when running the application in JEE_COMPATIBILITY mode.
* Use <code>evaluate(String, BrowserCallback)</code> instead.</p>
* <p>This method will throw an IllegalStateException if called while another script is still
* pending or executed. This can happen if called within a BrowserFunction, or if an SWT
* event is pending to be executed. (E.g. clicking a Button twice very fast.)
* </p>
* <!-- End RAP specific -->
*
* @param script the script with javascript commands
*
* @return the return value, if any, of executing the script
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the script is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_FAILED_EVALUATE when the script evaluation causes a javascript error to be thrown</li>
* <li>ERROR_INVALID_RETURN_VALUE when the script returns a value of unsupported type</li>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @exception UnsupportedOperationException when running the application in JEE_COMPATIBILITY mode
* @exception IllegalStateException when another script is already being executed.
* @see ProgressListener#completed(ProgressEvent)
* @see org.eclipse.rap.rwt.application.Application.OperationMode
*
* @since 1.4
*/
public Object evaluate( String script ) throws SWTException {
checkOperationMode();
if( script == null ) {
SWT.error( SWT.ERROR_NULL_ARGUMENT );
}
boolean success = execute( prepareScript( script ) );
if( !success ) {
throw createException();
}
return evaluateResult;
}
/**
* Executes the given script in a non-blocking way. The <code>browserCallback</code> is notified
* when the result from the operation is available.
* <p>
* Use this method instead of the <code>execute()</code> or <code>evaluate()</code> methods when
* running in <em>JEE_COMPATIBILITY</em> mode.
* </p>
*
* <p>
* This method will throw an IllegalStateException if called while another script is
* still pending to be executed.
* </p>
* @param script the script to execute, must not be <code>null</code>.
* @param browserCallback the callback to be notified when the result from the script execution is
* available, must not be <code>null</code>.
*
* @exception IllegalStateException when another script is already being executed.
*
* @see BrowserCallback
* @see org.eclipse.rap.rwt.application.Application.OperationMode
* @rwtextension This method is not available in SWT.
* @since 3.1
*/
public void evaluate( String script, BrowserCallback browserCallback ) {
ParamCheck.notNull( script, "script" );
ParamCheck.notNull( browserCallback, "browserCallback" );
evaluateNonBlocking( script, browserCallback );
}
/**
* Adds the listener to the collection of listeners who will be
* notified when the current location has changed or is about to change.
* <p>
* This notification typically occurs when the application navigates
* to a new location with {@link #setUrl(String)} or when the user
* activates a hyperlink.
* </p>
*
* @param listener the listener which should be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*/
public void addLocationListener( LocationListener listener ) {
checkWidget();
if( listener == null ) {
SWT.error( SWT.ERROR_NULL_ARGUMENT );
}
TypedBrowserListener browserListener = new TypedBrowserListener( listener );
addListener( EventTypes.LOCALTION_CHANGED, browserListener );
addListener( EventTypes.LOCALTION_CHANGING, browserListener );
}
/**
* Removes the listener from the collection of listeners who will
* be notified when the current location is changed or about to be changed.
*
* @param listener the listener which should no longer be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*/
public void removeLocationListener( LocationListener listener ) {
checkWidget();
removeListener( EventTypes.LOCALTION_CHANGED, listener );
removeListener( EventTypes.LOCALTION_CHANGING, listener );
}
/**
* Adds the listener to the collection of listeners who will be
* notified when a progress is made during the loading of the current
* URL or when the loading of the current URL has been completed.
*
* @param listener the listener which should be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @since 1.4
*/
public void addProgressListener( ProgressListener listener ) {
checkWidget();
if( listener == null ) {
SWT.error( SWT.ERROR_NULL_ARGUMENT );
}
TypedBrowserListener browserListener = new TypedBrowserListener( listener );
addListener( EventTypes.PROGRESS_CHANGED, browserListener );
addListener( EventTypes.PROGRESS_COMPLETED, browserListener );
}
/**
* Removes the listener from the collection of listeners who will
* be notified when a progress is made during the loading of the current
* URL or when the loading of the current URL has been completed.
*
* @param listener the listener which should no longer be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*
* @since 1.4
*/
public void removeProgressListener( ProgressListener listener ) {
checkWidget();
removeListener( EventTypes.PROGRESS_CHANGED, listener );
removeListener( EventTypes.PROGRESS_COMPLETED, listener );
}
@Override
@SuppressWarnings("unchecked")
public <T> T getAdapter( Class<T> adapter ) {
if( adapter == IBrowserAdapter.class ) {
if( browserAdapter == null ) {
browserAdapter = new BrowserAdapter();
}
return ( T )browserAdapter;
}
if( adapter == WidgetLCA.class ) {
return ( T )BrowserLCA.INSTANCE;
}
return super.getAdapter( adapter );
}
/**
* Returns the JavaXPCOM <code>nsIWebBrowser</code> for the receiver, or <code>null</code>
* if it is not available. In order for an <code>nsIWebBrowser</code> to be returned all
* of the following must be true: <ul>
* <li>the receiver's style must be <code>SWT.MOZILLA</code></li>
* <li>the classes from JavaXPCOM &gt;= 1.8.1.2 must be resolvable at runtime</li>
* <li>the version of the underlying XULRunner must be &gt;= 1.8.1.2</li>
* </ul>
*
* @return the receiver's JavaXPCOM <code>nsIWebBrowser</code> or <code>null</code>
*
* @since 1.4
*/
public Object getWebBrowser() {
checkWidget();
return null;
}
/**
* Returns the type of native browser being used by this instance.
*
* @return the type of the native browser
*
* @since 3.13
*/
public String getBrowserType() {
checkWidget();
return "iframe";
}
private static int checkStyle( int style ) {
int result = style;
if( ( style & ( SWT.MOZILLA | SWT.WEBKIT ) ) != 0 ) {
throw new SWTError( SWT.ERROR_NO_HANDLES, "Unsupported Browser type" );
}
if( ( result & SWT.H_SCROLL ) != 0 ) {
result &= ~SWT.H_SCROLL;
}
if( ( result & SWT.V_SCROLL ) != 0 ) {
result &= ~SWT.V_SCROLL;
}
return result;
}
//////////////////////////////////////////
// BrowserFunction support helping methods
private BrowserFunction[] getBrowserFunctions() {
return functions.toArray( new BrowserFunction[ functions.size() ] );
}
void createFunction( BrowserFunction function ) {
boolean removed = false;
for( int i = 0; !removed && i < functions.size(); i++ ) {
BrowserFunction current = functions.get( i );
if( current.name.equals( function.name ) ) {
functions.remove( current );
removed = true;
}
}
functions.add( function );
if( !removed ) {
updateBrowserFunctions( function.getName(), true );
}
}
void destroyFunction( BrowserFunction function ) {
functions.remove( function );
updateBrowserFunctions( function.getName(), false );
}
private void updateBrowserFunctions( String function, boolean create ) {
ServiceStore serviceStore = ContextProvider.getServiceStore();
String id = WidgetUtil.getId( this );
String key = create ? FUNCTIONS_TO_CREATE + id : FUNCTIONS_TO_DESTROY + id;
String[] funcList = ( String[] )serviceStore.getAttribute( key );
String[] newList;
if( funcList == null ) {
newList = new String[ 1 ];
newList[ 0 ] = function;
} else {
newList = new String[ funcList.length + 1 ];
System.arraycopy( funcList, 0, newList, 0, funcList.length );
newList[ funcList.length ] = function;
}
serviceStore.setAttribute( key, newList );
}
@Override
protected void checkWidget() {
super.checkWidget();
}
private static void checkOperationMode() {
if( getApplicationContext().getLifeCycleFactory().getLifeCycle() instanceof SimpleLifeCycle ) {
throw new UnsupportedOperationException( "Method not supported in JEE_COMPATIBILITY mode." );
}
}
private void onDispose() {
executeResult = Boolean.FALSE;
evaluateResult = null;
executeScript = null;
executePending = false;
}
//////////////////
// Helping methods
private boolean sendLocationChangingEvent( String location ) {
Event event = new Event();
event.text = location;
notifyListeners( EventTypes.LOCALTION_CHANGING, event );
return event.doit;
}
private void sendLocationChangedEvent( String location ) {
Event event = new Event();
event.text = location;
event.detail = SWT.TOP;
notifyListeners( EventTypes.LOCALTION_CHANGED, event );
}
private void sendProgressChangedEvent() {
notifyListeners( EventTypes.PROGRESS_CHANGED, new Event() );
}
private static String prepareScript( String script ) {
StringBuilder buffer = new StringBuilder( "(function(){" );
buffer.append( script );
buffer.append( "})();" );
return buffer.toString();
}
private void setExecuteResult( final boolean success, final Object result ) {
ProcessActionRunner.add( new Runnable() {
@Override
public void run() {
executeResult = Boolean.valueOf( success );
evaluateResult = result;
if( browserCallback != null ) {
if( success ) {
browserCallback.evaluationSucceeded( result );
} else {
browserCallback.evaluationFailed( createException() );
}
browserCallback = null;
executeScript = null;
executePending = false;
}
}
} );
}
private void evaluateNonBlocking( String script, BrowserCallback browserCallback ) {
checkWidget();
if( executeScript != null ) {
throw new IllegalStateException( "Another script is already pending" );
}
this.browserCallback = browserCallback;
executeScript = prepareScript( script );
}
private static SWTException createException() {
// TODO: Get the error message from the client
String errorString = "Failed to evaluate Javascript expression";
return new SWTException( SWT.ERROR_FAILED_EVALUATE, errorString );
}
////////////////
// Inner classes
private class BrowserDisposeListener implements DisposeListener {
@Override
public void widgetDisposed( DisposeEvent event ) {
onDispose();
}
}
private final class BrowserAdapter implements IBrowserAdapter {
@Override
public String getText() {
return html;
}
@Override
public String getExecuteScript() {
return executeScript;
}
@Override
public void setExecuteResult( boolean success, Object result ) {
Browser.this.setExecuteResult( success, result );
}
@Override
public void setExecutePending( boolean executePending ) {
Browser.this.executePending = executePending;
}
@Override
public boolean getExecutePending() {
return executePending;
}
@Override
public BrowserFunction[] getBrowserFunctions() {
return Browser.this.getBrowserFunctions();
}
@Override
public boolean hasUrlChanged() {
return urlChanged;
}
@Override
public void resetUrlChanged() {
urlChanged = false;
}
}
static class TypedBrowserListener extends TypedListener {
TypedBrowserListener( SWTEventListener listener ) {
super( listener );
}
@Override
public void handleEvent( Event event ) {
switch( event.type ) {
case EventTypes.LOCALTION_CHANGING: {
LocationListener locationListener = ( LocationListener )getEventListener();
LocationEvent locationEvent = new LocationEvent( event );
locationListener.changing( locationEvent );
event.doit = locationEvent.doit;
break;
}
case EventTypes.LOCALTION_CHANGED: {
LocationListener locationListener = ( LocationListener )getEventListener();
LocationEvent locationEvent = new LocationEvent( event );
locationListener.changed( locationEvent );
break;
}
case EventTypes.PROGRESS_CHANGED: {
ProgressListener progressListener = ( ProgressListener )getEventListener();
ProgressEvent progressEvent = new ProgressEvent( event );
progressListener.changed( progressEvent );
break;
}
case EventTypes.PROGRESS_COMPLETED: {
ProgressListener progressListener = ( ProgressListener )getEventListener();
ProgressEvent progressEvent = new ProgressEvent( event );
progressListener.completed( progressEvent );
break;
}
}
}
}
}