/******************************************************************************* * Copyright (c) 2002, 2016 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 >= 1.8.1.2 must be resolvable at runtime</li> * <li>the version of the underlying XULRunner must be >= 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; } 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; } } } } }