diff options
author | Artur Signell <artur@vaadin.com> | 2012-08-13 18:34:33 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2012-08-13 19:18:33 +0300 |
commit | e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch) | |
tree | 9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src/com/vaadin/terminal | |
parent | 14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff) | |
download | vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip |
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server/src/com/vaadin/terminal')
94 files changed, 19070 insertions, 0 deletions
diff --git a/server/src/com/vaadin/terminal/AbstractClientConnector.java b/server/src/com/vaadin/terminal/AbstractClientConnector.java new file mode 100644 index 0000000000..9c68361382 --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractClientConnector.java @@ -0,0 +1,510 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.logging.Logger; + +import com.vaadin.Application; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; +import com.vaadin.terminal.gwt.server.RpcManager; +import com.vaadin.terminal.gwt.server.RpcTarget; +import com.vaadin.terminal.gwt.server.ServerRpcManager; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.Root; + +/** + * An abstract base class for ClientConnector implementations. This class + * provides all the basic functionality required for connectors. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractClientConnector implements ClientConnector { + /** + * A map from client to server RPC interface class to the RPC call manager + * that handles incoming RPC calls for that interface. + */ + private Map<Class<?>, RpcManager> rpcManagerMap = new HashMap<Class<?>, RpcManager>(); + + /** + * A map from server to client RPC interface class to the RPC proxy that + * sends ourgoing RPC calls for that interface. + */ + private Map<Class<?>, ClientRpc> rpcProxyMap = new HashMap<Class<?>, ClientRpc>(); + + /** + * Shared state object to be communicated from the server to the client when + * modified. + */ + private SharedState sharedState; + + /** + * Pending RPC method invocations to be sent. + */ + private ArrayList<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); + + private String connectorId; + + private ArrayList<Extension> extensions = new ArrayList<Extension>(); + + private ClientConnector parent; + + /* Documentation copied from interface */ + @Override + public void requestRepaint() { + Root root = getRoot(); + if (root != null) { + root.getConnectorTracker().markDirty(this); + } + } + + /** + * Registers an RPC interface implementation for this component. + * + * A component can listen to multiple RPC interfaces, and subclasses can + * register additional implementations. + * + * @since 7.0 + * + * @param implementation + * RPC interface implementation + * @param rpcInterfaceType + * RPC interface class for which the implementation should be + * registered + */ + protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) { + rpcManagerMap.put(rpcInterfaceType, new ServerRpcManager<T>( + implementation, rpcInterfaceType)); + } + + /** + * Registers an RPC interface implementation for this component. + * + * A component can listen to multiple RPC interfaces, and subclasses can + * register additional implementations. + * + * @since 7.0 + * + * @param implementation + * RPC interface implementation. Also used to deduce the type. + */ + protected <T extends ServerRpc> void registerRpc(T implementation) { + Class<?> cls = implementation.getClass(); + Class<?>[] interfaces = cls.getInterfaces(); + while (interfaces.length == 0) { + // Search upwards until an interface is found. It must be found as T + // extends ServerRpc + cls = cls.getSuperclass(); + interfaces = cls.getInterfaces(); + } + if (interfaces.length != 1 + || !(ServerRpc.class.isAssignableFrom(interfaces[0]))) { + throw new RuntimeException( + "Use registerRpc(T implementation, Class<T> rpcInterfaceType) if the Rpc implementation implements more than one interface"); + } + @SuppressWarnings("unchecked") + Class<T> type = (Class<T>) interfaces[0]; + registerRpc(implementation, type); + } + + @Override + public SharedState getState() { + if (null == sharedState) { + sharedState = createState(); + } + return sharedState; + } + + /** + * Creates the shared state bean to be used in server to client + * communication. + * <p> + * By default a state object of the defined return type of + * {@link #getState()} is created. Subclasses can override this method and + * return a new instance of the correct state class but this should rarely + * be necessary. + * </p> + * <p> + * No configuration of the values of the state should be performed in + * {@link #createState()}. + * + * @since 7.0 + * + * @return new shared state object + */ + protected SharedState createState() { + try { + return getStateType().newInstance(); + } catch (Exception e) { + throw new RuntimeException( + "Error creating state of type " + getStateType().getName() + + " for " + getClass().getName(), e); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.server.ClientConnector#getStateType() + */ + @Override + public Class<? extends SharedState> getStateType() { + try { + Method m = getClass().getMethod("getState", (Class[]) null); + Class<?> type = m.getReturnType(); + return type.asSubclass(SharedState.class); + } catch (Exception e) { + throw new RuntimeException("Error finding state type for " + + getClass().getName(), e); + } + } + + /** + * Returns an RPC proxy for a given server to client RPC interface for this + * component. + * + * TODO more javadoc, subclasses, ... + * + * @param rpcInterface + * RPC interface type + * + * @since 7.0 + */ + public <T extends ClientRpc> T getRpcProxy(final Class<T> rpcInterface) { + // create, initialize and return a dynamic proxy for RPC + try { + if (!rpcProxyMap.containsKey(rpcInterface)) { + Class<?> proxyClass = Proxy.getProxyClass( + rpcInterface.getClassLoader(), rpcInterface); + Constructor<?> constructor = proxyClass + .getConstructor(InvocationHandler.class); + T rpcProxy = rpcInterface.cast(constructor + .newInstance(new RpcInvoicationHandler(rpcInterface))); + // cache the proxy + rpcProxyMap.put(rpcInterface, rpcProxy); + } + return (T) rpcProxyMap.get(rpcInterface); + } catch (Exception e) { + // TODO exception handling? + throw new RuntimeException(e); + } + } + + private static final class AllChildrenIterable implements + Iterable<ClientConnector>, Serializable { + private final ClientConnector connector; + + private AllChildrenIterable(ClientConnector connector) { + this.connector = connector; + } + + @Override + public Iterator<ClientConnector> iterator() { + CombinedIterator<ClientConnector> iterator = new CombinedIterator<ClientConnector>(); + iterator.addIterator(connector.getExtensions().iterator()); + + if (connector instanceof HasComponents) { + HasComponents hasComponents = (HasComponents) connector; + iterator.addIterator(hasComponents.iterator()); + } + + return iterator; + } + } + + private class RpcInvoicationHandler implements InvocationHandler, + Serializable { + + private String rpcInterfaceName; + + public RpcInvoicationHandler(Class<?> rpcInterface) { + rpcInterfaceName = rpcInterface.getName().replaceAll("\\$", "."); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + addMethodInvocationToQueue(rpcInterfaceName, method, args); + // TODO no need to do full repaint if only RPC calls + requestRepaint(); + return null; + } + + } + + /** + * For internal use: adds a method invocation to the pending RPC call queue. + * + * @param interfaceName + * RPC interface name + * @param method + * RPC method + * @param parameters + * RPC all parameters + * + * @since 7.0 + */ + protected void addMethodInvocationToQueue(String interfaceName, + Method method, Object[] parameters) { + // add to queue + pendingInvocations.add(new ClientMethodInvocation(this, interfaceName, + method, parameters)); + } + + /** + * @see RpcTarget#getRpcManager(Class) + * + * @param rpcInterface + * RPC interface for which a call was made + * @return RPC Manager handling calls for the interface + * + * @since 7.0 + */ + @Override + public RpcManager getRpcManager(Class<?> rpcInterface) { + return rpcManagerMap.get(rpcInterface); + } + + @Override + public List<ClientMethodInvocation> retrievePendingRpcCalls() { + if (pendingInvocations.isEmpty()) { + return Collections.emptyList(); + } else { + List<ClientMethodInvocation> result = pendingInvocations; + pendingInvocations = new ArrayList<ClientMethodInvocation>(); + return Collections.unmodifiableList(result); + } + } + + @Override + public String getConnectorId() { + if (connectorId == null) { + if (getApplication() == null) { + throw new RuntimeException( + "Component must be attached to an application when getConnectorId() is called for the first time"); + } + connectorId = getApplication().createConnectorId(this); + } + return connectorId; + } + + /** + * Finds the Application to which this connector belongs. If the connector + * has not been attached, <code>null</code> is returned. + * + * @return The connector's application, or <code>null</code> if not attached + */ + protected Application getApplication() { + Root root = getRoot(); + if (root == null) { + return null; + } else { + return root.getApplication(); + } + } + + /** + * Finds a Root ancestor of this connector. <code>null</code> is returned if + * no Root ancestor is found (typically because the connector is not + * attached to a proper hierarchy). + * + * @return the Root ancestor of this connector, or <code>null</code> if none + * is found. + */ + @Override + public Root getRoot() { + ClientConnector connector = this; + while (connector != null) { + if (connector instanceof Root) { + return (Root) connector; + } + connector = connector.getParent(); + } + return null; + } + + private static Logger getLogger() { + return Logger.getLogger(AbstractClientConnector.class.getName()); + } + + @Override + public void requestRepaintAll() { + requestRepaint(); + + for (ClientConnector connector : getAllChildrenIterable(this)) { + connector.requestRepaintAll(); + } + } + + private static final class CombinedIterator<T> implements Iterator<T>, + Serializable { + + private final Collection<Iterator<? extends T>> iterators = new ArrayList<Iterator<? extends T>>(); + + public void addIterator(Iterator<? extends T> iterator) { + iterators.add(iterator); + } + + @Override + public boolean hasNext() { + for (Iterator<? extends T> i : iterators) { + if (i.hasNext()) { + return true; + } + } + return false; + } + + @Override + public T next() { + for (Iterator<? extends T> i : iterators) { + if (i.hasNext()) { + return i.next(); + } + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * Get an Iterable for iterating over all child connectors, including both + * extensions and child components. + * + * @param connector + * the connector to get children for + * @return an Iterable giving all child connectors. + */ + public static Iterable<ClientConnector> getAllChildrenIterable( + final ClientConnector connector) { + return new AllChildrenIterable(connector); + } + + @Override + public Collection<Extension> getExtensions() { + return Collections.unmodifiableCollection(extensions); + } + + /** + * Add an extension to this connector. This method is protected to allow + * extensions to select which targets they can extend. + * + * @param extension + * the extension to add + */ + protected void addExtension(Extension extension) { + ClientConnector previousParent = extension.getParent(); + if (previousParent == this) { + // Nothing to do, already attached + return; + } else if (previousParent != null) { + throw new IllegalStateException( + "Moving an extension from one parent to another is not supported"); + } + + extensions.add(extension); + extension.setParent(this); + requestRepaint(); + } + + @Override + public void removeExtension(Extension extension) { + extension.setParent(null); + extensions.remove(extension); + requestRepaint(); + } + + @Override + public void setParent(ClientConnector parent) { + + // If the parent is not changed, don't do anything + if (parent == this.parent) { + return; + } + + if (parent != null && this.parent != null) { + throw new IllegalStateException(getClass().getName() + + " already has a parent."); + } + + // Send detach event if the component have been connected to a window + if (getApplication() != null) { + detach(); + } + + // Connect to new parent + this.parent = parent; + + // Send attach event if connected to an application + if (getApplication() != null) { + attach(); + } + } + + @Override + public ClientConnector getParent() { + return parent; + } + + @Override + public void attach() { + requestRepaint(); + + getRoot().getConnectorTracker().registerConnector(this); + + for (ClientConnector connector : getAllChildrenIterable(this)) { + connector.attach(); + } + + } + + /** + * {@inheritDoc} + * + * <p> + * The {@link #getApplication()} and {@link #getRoot()} methods might return + * <code>null</code> after this method is called. + * </p> + */ + @Override + public void detach() { + for (ClientConnector connector : getAllChildrenIterable(this)) { + connector.detach(); + } + + getRoot().getConnectorTracker().unregisterConnector(this); + } + + @Override + public boolean isConnectorEnabled() { + if (getParent() == null) { + // No parent -> the component cannot receive updates from the client + return false; + } else { + return getParent().isConnectorEnabled(); + } + } +} diff --git a/server/src/com/vaadin/terminal/AbstractErrorMessage.java b/server/src/com/vaadin/terminal/AbstractErrorMessage.java new file mode 100644 index 0000000000..f7cd0e6aad --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractErrorMessage.java @@ -0,0 +1,176 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.data.Buffered; +import com.vaadin.data.Validator; +import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; + +/** + * Base class for component error messages. + * + * This class is used on the server side to construct the error messages to send + * to the client. + * + * @since 7.0 + */ +public abstract class AbstractErrorMessage implements ErrorMessage { + + public enum ContentMode { + /** + * Content mode, where the error contains only plain text. + */ + TEXT, + /** + * Content mode, where the error contains preformatted text. + */ + PREFORMATTED, + /** + * Content mode, where the error contains XHTML. + */ + XHTML; + } + + /** + * Content mode. + */ + private ContentMode mode = ContentMode.TEXT; + + /** + * Message in content mode. + */ + private String message; + + /** + * Error level. + */ + private ErrorLevel level = ErrorLevel.ERROR; + + private List<ErrorMessage> causes = new ArrayList<ErrorMessage>(); + + protected AbstractErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + protected void setMessage(String message) { + this.message = message; + } + + /* Documented in interface */ + @Override + public ErrorLevel getErrorLevel() { + return level; + } + + protected void setErrorLevel(ErrorLevel level) { + this.level = level; + } + + protected ContentMode getMode() { + return mode; + } + + protected void setMode(ContentMode mode) { + this.mode = mode; + } + + protected List<ErrorMessage> getCauses() { + return causes; + } + + protected void addCause(ErrorMessage cause) { + causes.add(cause); + } + + @Override + public String getFormattedHtmlMessage() { + String result = null; + switch (getMode()) { + case TEXT: + result = AbstractApplicationServlet.safeEscapeForHtml(getMessage()); + break; + case PREFORMATTED: + result = "<pre>" + + AbstractApplicationServlet + .safeEscapeForHtml(getMessage()) + "</pre>"; + break; + case XHTML: + result = getMessage(); + break; + } + // if no message, combine the messages of all children + if (null == result && null != getCauses() && getCauses().size() > 0) { + StringBuilder sb = new StringBuilder(); + for (ErrorMessage cause : getCauses()) { + String childMessage = cause.getFormattedHtmlMessage(); + if (null != childMessage) { + sb.append("<div>"); + sb.append(childMessage); + sb.append("</div>\n"); + } + } + if (sb.length() > 0) { + result = sb.toString(); + } + } + // still no message? use an empty string for backwards compatibility + if (null == result) { + result = ""; + } + return result; + } + + // TODO replace this with a helper method elsewhere? + public static ErrorMessage getErrorMessageForException(Throwable t) { + if (null == t) { + return null; + } else if (t instanceof ErrorMessage) { + // legacy case for custom error messages + return (ErrorMessage) t; + } else if (t instanceof Validator.InvalidValueException) { + UserError error = new UserError( + ((Validator.InvalidValueException) t).getHtmlMessage(), + ContentMode.XHTML, ErrorLevel.ERROR); + for (Validator.InvalidValueException nestedException : ((Validator.InvalidValueException) t) + .getCauses()) { + error.addCause(getErrorMessageForException(nestedException)); + } + return error; + } else if (t instanceof Buffered.SourceException) { + // no message, only the causes to be painted + UserError error = new UserError(null); + // in practice, this was always ERROR in Vaadin 6 unless tweaked in + // custom exceptions implementing ErrorMessage + error.setErrorLevel(ErrorLevel.ERROR); + // causes + for (Throwable nestedException : ((Buffered.SourceException) t) + .getCauses()) { + error.addCause(getErrorMessageForException(nestedException)); + } + return error; + } else { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + return new SystemError(sw.toString()); + } + } + + /* Documented in superclass */ + @Override + public String toString() { + return getMessage(); + } + +} diff --git a/server/src/com/vaadin/terminal/AbstractExtension.java b/server/src/com/vaadin/terminal/AbstractExtension.java new file mode 100644 index 0000000000..33a60e39ef --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractExtension.java @@ -0,0 +1,76 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * An extension is an entity that is attached to a Component or another + * Extension and independently communicates between client and server. + * <p> + * Extensions can use shared state and RPC in the same way as components. + * <p> + * AbstractExtension adds a mechanism for adding the extension to any Connector + * (extend). To let the Extension determine what kind target it can be added to, + * the extend method is declared as protected. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractExtension extends AbstractClientConnector + implements Extension { + private boolean previouslyAttached = false; + + /** + * Gets a type that the parent must be an instance of. Override this if the + * extension only support certain targets, e.g. if only TextFields can be + * extended. + * + * @return a type that the parent must be an instance of + */ + protected Class<? extends ClientConnector> getSupportedParentType() { + return ClientConnector.class; + } + + /** + * Add this extension to the target connector. This method is protected to + * allow subclasses to require a more specific type of target. + * + * @param target + * the connector to attach this extension to + */ + protected void extend(AbstractClientConnector target) { + target.addExtension(this); + } + + /** + * Remove this extension from its target. After an extension has been + * removed, it can not be attached again. + */ + public void removeFromTarget() { + getParent().removeExtension(this); + } + + @Override + public void setParent(ClientConnector parent) { + if (previouslyAttached && parent != null) { + throw new IllegalStateException( + "An extension can not be set to extend a new target after getting detached from the previous."); + } + + Class<? extends ClientConnector> supportedParentType = getSupportedParentType(); + if (parent == null || supportedParentType.isInstance(parent)) { + super.setParent(parent); + previouslyAttached = true; + } else { + throw new IllegalArgumentException(getClass().getName() + + " can only be attached to targets of type " + + supportedParentType.getName() + " but attach to " + + parent.getClass().getName() + " was attempted."); + } + } + +} diff --git a/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java b/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java new file mode 100644 index 0000000000..7bafb6d2b3 --- /dev/null +++ b/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java @@ -0,0 +1,162 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.shared.JavaScriptExtensionState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.JavaScriptFunction; + +/** + * Base class for Extensions with all client-side logic implemented using + * JavaScript. + * <p> + * When a new JavaScript extension is initialized in the browser, the framework + * will look for a globally defined JavaScript function that will initialize the + * extension. The name of the initialization function is formed by replacing . + * with _ in the name of the server-side class. If no such function is defined, + * each super class is used in turn until a match is found. The framework will + * thus first attempt with <code>com_example_MyExtension</code> for the + * server-side + * <code>com.example.MyExtension extends AbstractJavaScriptExtension</code> + * class. If MyExtension instead extends <code>com.example.SuperExtension</code> + * , then <code>com_example_SuperExtension</code> will also be attempted if + * <code>com_example_MyExtension</code> has not been defined. + * <p> + * + * The initialization function will be called with <code>this</code> pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + * <ul> + * <li><code>getConnectorId()</code> - returns a string with the id of the + * connector.</li> + * <li><code>getParentId([connectorId])</code> - returns a string with the id of + * the connector's parent. If <code>connectorId</code> is provided, the id of + * the parent of the corresponding connector with the passed id is returned + * instead.</li> + * <li><code>getElement([connectorId])</code> - returns the DOM Element that is + * the root of a connector's widget. <code>null</code> is returned if the + * connector can not be found or if the connector doesn't have a widget. If + * <code>connectorId</code> is not provided, the connector id of the current + * connector will be used.</li> + * <li><code>getState()</code> - returns an object corresponding to the shared + * state defined on the server. The scheme for conversion between Java and + * JavaScript types is described bellow.</li> + * <li><code>registerRpc([name, ] rpcObject)</code> - registers the + * <code>rpcObject</code> as a RPC handler. <code>rpcObject</code> should be an + * object with field containing functions for all eligible RPC functions. If + * <code>name</code> is provided, the RPC handler will only used for RPC calls + * for the RPC interface with the same fully qualified Java name. If no + * <code>name</code> is provided, the RPC handler will be used for all incoming + * RPC invocations where the RPC method name is defined as a function field in + * the handler. The scheme for conversion between Java types in the RPC + * interface definition and the JavaScript values passed as arguments to the + * handler functions is described bellow.</li> + * <li><code>getRpcProxy([name])</code> - returns an RPC proxy object. If + * <code>name</code> is provided, the proxy object will contain functions for + * all methods in the RPC interface with the same fully qualified name, provided + * a RPC handler has been registered by the server-side code. If no + * <code>name</code> is provided, the returned RPC proxy object will contain + * functions for all methods in all RPC interfaces registered for the connector + * on the server. If the same method name is present in multiple registered RPC + * interfaces, the corresponding function in the RPC proxy object will throw an + * exception when called. The scheme for conversion between Java types in the + * RPC interface and the JavaScript values that should be passed to the + * functions is described bellow.</li> + * <li><code>translateVaadinUri(uri)</code> - Translates a Vaadin URI to a URL + * that can be used in the browser. This is just way of accessing + * {@link ApplicationConnection#translateVaadinUri(String)}</li> + * </ul> + * The connector wrapper also supports these special functions: + * <ul> + * <li><code>onStateChange</code> - If the JavaScript code assigns a function to + * the field, that function is called whenever the contents of the shared state + * is changed.</li> + * <li>Any field name corresponding to a call to + * {@link #addFunction(String, JavaScriptFunction)} on the server will + * automatically be present as a function that triggers the registered function + * on the server.</li> + * <li>Any field name referred to using + * {@link #callFunction(String, Object...)} on the server will be called if a + * function has been assigned to the field.</li> + * </ul> + * <p> + * + * Values in the Shared State and in RPC calls are converted between Java and + * JavaScript using the following conventions: + * <ul> + * <li>Primitive Java numbers (byte, char, int, long, float, double) and their + * boxed types (Byte, Character, Integer, Long, Float, Double) are represented + * by JavaScript numbers.</li> + * <li>The primitive Java boolean and the boxed Boolean are represented by + * JavaScript booleans.</li> + * <li>Java Strings are represented by JavaScript strings.</li> + * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li> + * <li>Map<String, ?> in Java is represented by JavaScript object with fields + * corresponding to the map keys.</li> + * <li>Any other Java Map is represented by a JavaScript array containing two + * arrays, the first contains the keys and the second contains the values in the + * same order.</li> + * <li>A Java Bean is represented by a JavaScript object with fields + * corresponding to the bean's properties.</li> + * <li>A Java Connector is represented by a JavaScript string containing the + * connector's id.</li> + * <li>A pluggable serialization mechanism is provided for types not described + * here. Please refer to the documentation for specific types for serialization + * information.</li> + * </ul> + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractJavaScriptExtension extends AbstractExtension { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + @Override + protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) { + super.registerRpc(implementation, rpcInterfaceType); + callbackHelper.registerRpc(rpcInterfaceType); + } + + /** + * Register a {@link JavaScriptFunction} that can be called from the + * JavaScript using the provided name. A JavaScript function with the + * provided name will be added to the connector wrapper object (initially + * available as <code>this</code>). Calling that JavaScript function will + * cause the call method in the registered {@link JavaScriptFunction} to be + * invoked with the same arguments. + * + * @param functionName + * the name that should be used for client-side callback + * @param function + * the {@link JavaScriptFunction} object that will be invoked + * when the JavaScript function is called + */ + protected void addFunction(String functionName, JavaScriptFunction function) { + callbackHelper.registerCallback(functionName, function); + } + + /** + * Invoke a named function that the connector JavaScript has added to the + * JavaScript connector wrapper object. The arguments should only contain + * data types that can be represented in JavaScript including primitives, + * their boxed types, arrays, String, List, Set, Map, Connector and + * JavaBeans. + * + * @param name + * the name of the function + * @param arguments + * function arguments + */ + protected void callFunction(String name, Object... arguments) { + callbackHelper.invokeCallback(name, arguments); + } + + @Override + public JavaScriptExtensionState getState() { + return (JavaScriptExtensionState) super.getState(); + } +} diff --git a/server/src/com/vaadin/terminal/ApplicationResource.java b/server/src/com/vaadin/terminal/ApplicationResource.java new file mode 100644 index 0000000000..da92642d02 --- /dev/null +++ b/server/src/com/vaadin/terminal/ApplicationResource.java @@ -0,0 +1,75 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; + +/** + * This interface must be implemented by classes wishing to provide Application + * resources. + * <p> + * <code>ApplicationResource</code> are a set of named resources (pictures, + * sounds, etc) associated with some specific application. Having named + * application resources provides a convenient method for having inter-theme + * common resources for an application. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ApplicationResource extends Resource, Serializable { + + /** + * Default cache time. + */ + public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24; + + /** + * Gets resource as stream. + */ + public DownloadStream getStream(); + + /** + * Gets the application of the resource. + */ + public Application getApplication(); + + /** + * Gets the virtual filename for this resource. + * + * @return the file name associated to this resource. + */ + public String getFilename(); + + /** + * Gets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Default is <code>DEFAULT_CACHETIME</code>. + * </p> + * + * @return Cache time in milliseconds + */ + public long getCacheTime(); + + /** + * Gets the size of the download buffer used for this resource. + * + * <p> + * If the buffer size is 0, the buffer size is decided by the terminal + * adapter. The default value is 0. + * </p> + * + * @return int the size of the buffer in bytes. + */ + public int getBufferSize(); + +} diff --git a/server/src/com/vaadin/terminal/ClassResource.java b/server/src/com/vaadin/terminal/ClassResource.java new file mode 100644 index 0000000000..b74c8e7bb7 --- /dev/null +++ b/server/src/com/vaadin/terminal/ClassResource.java @@ -0,0 +1,178 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ClassResource</code> is a named resource accessed with the class + * loader. + * + * This can be used to access resources such as icons, files, etc. + * + * @see java.lang.Class#getResource(java.lang.String) + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ClassResource implements ApplicationResource, Serializable { + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DEFAULT_CACHETIME; + + /** + * Associated class used for indetifying the source of the resource. + */ + private final Class<?> associatedClass; + + /** + * Name of the resource is relative to the associated class. + */ + private final String resourceName; + + /** + * Application used for serving the class. + */ + private final Application application; + + /** + * Creates a new application resource instance. The resource id is relative + * to the location of the application class. + * + * @param resourceName + * the Unique identifier of the resource within the application. + * @param application + * the application this resource will be added to. + */ + public ClassResource(String resourceName, Application application) { + this(application.getClass(), resourceName, application); + } + + /** + * Creates a new application resource instance. + * + * @param associatedClass + * the class of the which the resource is associated. + * @param resourceName + * the Unique identifier of the resource within the application. + * @param application + * the application this resource will be added to. + */ + public ClassResource(Class<?> associatedClass, String resourceName, + Application application) { + this.associatedClass = associatedClass; + this.resourceName = resourceName; + this.application = application; + if (resourceName == null || associatedClass == null) { + throw new NullPointerException(); + } + application.addResource(this); + } + + /** + * Gets the MIME type of this resource. + * + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + return FileTypeResolver.getMIMEType(resourceName); + } + + /** + * Gets the application of this resource. + * + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + @Override + public Application getApplication() { + return application; + } + + /** + * Gets the virtual filename for this resource. + * + * @return the file name associated to this resource. + * @see com.vaadin.terminal.ApplicationResource#getFilename() + */ + @Override + public String getFilename() { + int index = 0; + int next = 0; + while ((next = resourceName.indexOf('/', index)) > 0 + && next + 1 < resourceName.length()) { + index = next + 1; + } + return resourceName.substring(index); + } + + /** + * Gets resource as stream. + * + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + @Override + public DownloadStream getStream() { + final DownloadStream ds = new DownloadStream( + associatedClass.getResourceAsStream(resourceName), + getMIMEType(), getFilename()); + ds.setBufferSize(getBufferSize()); + ds.setCacheTime(cacheTime); + return ds; + } + + /* documented in superclass */ + @Override + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /* documented in superclass */ + @Override + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Zero or negavive value disbales the caching of this + * stream. + * </p> + * + * @param cacheTime + * the cache time in milliseconds. + * + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } +} diff --git a/server/src/com/vaadin/terminal/CombinedRequest.java b/server/src/com/vaadin/terminal/CombinedRequest.java new file mode 100644 index 0000000000..5b92feb39a --- /dev/null +++ b/server/src/com/vaadin/terminal/CombinedRequest.java @@ -0,0 +1,187 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.terminal.gwt.server.WebApplicationContext; +import com.vaadin.terminal.gwt.server.WebBrowser; + +/** + * A {@link WrappedRequest} with path and parameters from one request and + * {@link WrappedRequest.BrowserDetails} extracted from another request. + * + * This class is intended to be used for a two request initialization where the + * first request fetches the actual application page and the second request + * contains information extracted from the browser using javascript. + * + */ +public class CombinedRequest implements WrappedRequest { + + private final WrappedRequest secondRequest; + private Map<String, String[]> parameterMap; + + /** + * Creates a new combined request based on the second request and some + * details from the first request. + * + * @param secondRequest + * the second request which will be used as the foundation of the + * combined request + * @throws JSONException + * if the initialParams parameter can not be decoded + */ + public CombinedRequest(WrappedRequest secondRequest) throws JSONException { + this.secondRequest = secondRequest; + + HashMap<String, String[]> map = new HashMap<String, String[]>(); + JSONObject initialParams = new JSONObject( + secondRequest.getParameter("initialParams")); + for (Iterator<?> keys = initialParams.keys(); keys.hasNext();) { + String name = (String) keys.next(); + JSONArray jsonValues = initialParams.getJSONArray(name); + String[] values = new String[jsonValues.length()]; + for (int i = 0; i < values.length; i++) { + values[i] = jsonValues.getString(i); + } + map.put(name, values); + } + + parameterMap = Collections.unmodifiableMap(map); + + } + + @Override + public String getParameter(String parameter) { + String[] strings = getParameterMap().get(parameter); + if (strings == null || strings.length == 0) { + return null; + } else { + return strings[0]; + } + } + + @Override + public Map<String, String[]> getParameterMap() { + return parameterMap; + } + + @Override + public int getContentLength() { + return secondRequest.getContentLength(); + } + + @Override + public InputStream getInputStream() throws IOException { + return secondRequest.getInputStream(); + } + + @Override + public Object getAttribute(String name) { + return secondRequest.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + secondRequest.setAttribute(name, value); + } + + @Override + public String getRequestPathInfo() { + return secondRequest.getParameter("initialPath"); + } + + @Override + public int getSessionMaxInactiveInterval() { + return secondRequest.getSessionMaxInactiveInterval(); + } + + @Override + public Object getSessionAttribute(String name) { + return secondRequest.getSessionAttribute(name); + } + + @Override + public void setSessionAttribute(String name, Object attribute) { + secondRequest.setSessionAttribute(name, attribute); + } + + @Override + public String getContentType() { + return secondRequest.getContentType(); + } + + @Override + public BrowserDetails getBrowserDetails() { + return new BrowserDetails() { + @Override + public String getUriFragment() { + String fragment = secondRequest.getParameter("fr"); + if (fragment == null) { + return ""; + } else { + return fragment; + } + } + + @Override + public String getWindowName() { + return secondRequest.getParameter("wn"); + } + + @Override + public WebBrowser getWebBrowser() { + WebApplicationContext context = (WebApplicationContext) Application + .getCurrent().getContext(); + return context.getBrowser(); + } + }; + } + + /** + * Gets the original second request. This can be used e.g. if a request + * parameter from the second request is required. + * + * @return the original second wrapped request + */ + public WrappedRequest getSecondRequest() { + return secondRequest; + } + + @Override + public Locale getLocale() { + return secondRequest.getLocale(); + } + + @Override + public String getRemoteAddr() { + return secondRequest.getRemoteAddr(); + } + + @Override + public boolean isSecure() { + return secondRequest.isSecure(); + } + + @Override + public String getHeader(String name) { + return secondRequest.getHeader(name); + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return secondRequest.getDeploymentConfiguration(); + } +} diff --git a/server/src/com/vaadin/terminal/CompositeErrorMessage.java b/server/src/com/vaadin/terminal/CompositeErrorMessage.java new file mode 100644 index 0000000000..b82b622f54 --- /dev/null +++ b/server/src/com/vaadin/terminal/CompositeErrorMessage.java @@ -0,0 +1,112 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Class for combining multiple error messages together. + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CompositeErrorMessage extends AbstractErrorMessage { + + /** + * Constructor for CompositeErrorMessage. + * + * @param errorMessages + * the Array of error messages that are listed togeter. Nulls are + * ignored, but at least one message is required. + */ + public CompositeErrorMessage(ErrorMessage[] errorMessages) { + super(null); + setErrorLevel(ErrorLevel.INFORMATION); + + for (int i = 0; i < errorMessages.length; i++) { + addErrorMessage(errorMessages[i]); + } + + if (getCauses().size() == 0) { + throw new IllegalArgumentException( + "Composite error message must have at least one error"); + } + + } + + /** + * Constructor for CompositeErrorMessage. + * + * @param errorMessages + * the Collection of error messages that are listed together. At + * least one message is required. + */ + public CompositeErrorMessage( + Collection<? extends ErrorMessage> errorMessages) { + super(null); + setErrorLevel(ErrorLevel.INFORMATION); + + for (final Iterator<? extends ErrorMessage> i = errorMessages + .iterator(); i.hasNext();) { + addErrorMessage(i.next()); + } + + if (getCauses().size() == 0) { + throw new IllegalArgumentException( + "Composite error message must have at least one error"); + } + } + + /** + * Adds a error message into this composite message. Updates the level + * field. + * + * @param error + * the error message to be added. Duplicate errors are ignored. + */ + private void addErrorMessage(ErrorMessage error) { + if (error != null && !getCauses().contains(error)) { + addCause(error); + if (error.getErrorLevel().intValue() > getErrorLevel().intValue()) { + setErrorLevel(error.getErrorLevel()); + } + } + } + + /** + * Gets Error Iterator. + * + * @return the error iterator. + */ + public Iterator<ErrorMessage> iterator() { + return getCauses().iterator(); + } + + /** + * Returns a comma separated list of the error messages. + * + * @return String, comma separated list of error messages. + */ + @Override + public String toString() { + String retval = "["; + int pos = 0; + for (final Iterator<ErrorMessage> i = getCauses().iterator(); i + .hasNext();) { + if (pos > 0) { + retval += ","; + } + pos++; + retval += i.next().toString(); + } + retval += "]"; + + return retval; + } +} diff --git a/server/src/com/vaadin/terminal/DeploymentConfiguration.java b/server/src/com/vaadin/terminal/DeploymentConfiguration.java new file mode 100644 index 0000000000..ae96dcaec5 --- /dev/null +++ b/server/src/com/vaadin/terminal/DeploymentConfiguration.java @@ -0,0 +1,123 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.Properties; + +import javax.portlet.PortletContext; +import javax.servlet.ServletContext; + +import com.vaadin.terminal.gwt.server.AddonContext; +import com.vaadin.terminal.gwt.server.AddonContextListener; + +/** + * Provide deployment specific settings that are required outside terminal + * specific code. + * + * @author Vaadin Ltd. + * + * @since 7.0 + */ +public interface DeploymentConfiguration extends Serializable { + + /** + * Gets the base URL of the location of Vaadin's static files. + * + * @param request + * the request for which the location should be determined + * + * @return a string with the base URL for static files + */ + public String getStaticFileLocation(WrappedRequest request); + + /** + * Gets the widgetset that is configured for this deployment, e.g. from a + * parameter in web.xml. + * + * @param request + * the request for which a widgetset is required + * @return the name of the widgetset + */ + public String getConfiguredWidgetset(WrappedRequest request); + + /** + * Gets the theme that is configured for this deployment, e.g. from a portal + * parameter or just some sensible default value. + * + * @param request + * the request for which a theme is required + * @return the name of the theme + */ + public String getConfiguredTheme(WrappedRequest request); + + /** + * Checks whether the Vaadin application will be rendered on its own in the + * browser or whether it will be included into some other context. A + * standalone application may do things that might interfere with other + * parts of a page, e.g. changing the page title and requesting focus upon + * loading. + * + * @param request + * the request for which the application is loaded + * @return a boolean indicating whether the application should be standalone + */ + public boolean isStandalone(WrappedRequest request); + + /** + * Gets a configured property. The properties are typically read from e.g. + * web.xml or from system properties of the JVM. + * + * @param propertyName + * The simple of the property, in some contexts, lookup might be + * performed using variations of the provided name. + * @param defaultValue + * the default value that should be used if no value has been + * defined + * @return the property value, or the passed default value if no property + * value is found + */ + public String getApplicationOrSystemProperty(String propertyName, + String defaultValue); + + /** + * Get the class loader to use for loading classes loaded by name, e.g. + * custom Root classes. <code>null</code> indicates that the default class + * loader should be used. + * + * @return the class loader to use, or <code>null</code> + */ + public ClassLoader getClassLoader(); + + /** + * Returns the MIME type of the specified file, or null if the MIME type is + * not known. The MIME type is determined by the configuration of the + * container, and may be specified in a deployment descriptor. Common MIME + * types are "text/html" and "image/gif". + * + * @param resourceName + * a String specifying the name of a file + * @return a String specifying the file's MIME type + * + * @see ServletContext#getMimeType(String) + * @see PortletContext#getMimeType(String) + */ + public String getMimeType(String resourceName); + + /** + * Gets the properties configured for the deployment, e.g. as init + * parameters to the servlet or portlet. + * + * @return properties for the application. + */ + public Properties getInitParameters(); + + public Iterator<AddonContextListener> getAddonContextListeners(); + + public AddonContext getAddonContext(); + + public void setAddonContext(AddonContext vaadinContext); +} diff --git a/server/src/com/vaadin/terminal/DownloadStream.java b/server/src/com/vaadin/terminal/DownloadStream.java new file mode 100644 index 0000000000..9853b0eee2 --- /dev/null +++ b/server/src/com/vaadin/terminal/DownloadStream.java @@ -0,0 +1,335 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.terminal.gwt.server.Constants; + +/** + * Downloadable stream. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class DownloadStream implements Serializable { + + /** + * Maximum cache time. + */ + public static final long MAX_CACHETIME = Long.MAX_VALUE; + + /** + * Default cache time. + */ + public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24; + + private InputStream stream; + + private String contentType; + + private String fileName; + + private Map<String, String> params; + + private long cacheTime = DEFAULT_CACHETIME; + + private int bufferSize = 0; + + /** + * Creates a new instance of DownloadStream. + */ + public DownloadStream(InputStream stream, String contentType, + String fileName) { + setStream(stream); + setContentType(contentType); + setFileName(fileName); + } + + /** + * Gets downloadable stream. + * + * @return output stream. + */ + public InputStream getStream() { + return stream; + } + + /** + * Sets the stream. + * + * @param stream + * The stream to set + */ + public void setStream(InputStream stream) { + this.stream = stream; + } + + /** + * Gets stream content type. + * + * @return type of the stream content. + */ + public String getContentType() { + return contentType; + } + + /** + * Sets stream content type. + * + * @param contentType + * the contentType to set + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Returns the file name. + * + * @return the name of the file. + */ + public String getFileName() { + return fileName; + } + + /** + * Sets the file name. + * + * @param fileName + * the file name to set. + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * Sets a paramater for download stream. Parameters are optional information + * about the downloadable stream and their meaning depends on the used + * adapter. For example in WebAdapter they are interpreted as HTTP response + * headers. + * + * If the parameters by this name exists, the old value is replaced. + * + * @param name + * the Name of the parameter to set. + * @param value + * the Value of the parameter to set. + */ + public void setParameter(String name, String value) { + if (params == null) { + params = new HashMap<String, String>(); + } + params.put(name, value); + } + + /** + * Gets a paramater for download stream. Parameters are optional information + * about the downloadable stream and their meaning depends on the used + * adapter. For example in WebAdapter they are interpreted as HTTP response + * headers. + * + * @param name + * the Name of the parameter to set. + * @return Value of the parameter or null if the parameter does not exist. + */ + public String getParameter(String name) { + if (params != null) { + return params.get(name); + } + return null; + } + + /** + * Gets the names of the parameters. + * + * @return Iterator of names or null if no parameters are set. + */ + public Iterator<String> getParameterNames() { + if (params != null) { + return params.keySet().iterator(); + } + return null; + } + + /** + * Gets length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Default is + * <code>DEFAULT_CACHETIME</code>. + * + * @return Cache time in milliseconds + */ + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Zero or negavive + * value disbales the caching of this stream. + * + * @param cacheTime + * the cache time in milliseconds. + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + + /** + * Gets the size of the download buffer. + * + * @return int The size of the buffer in bytes. + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer. + * + * @param bufferSize + * the size of the buffer in bytes. + * + * @since 7.0 + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /** + * Writes this download stream to a wrapped response. This takes care of + * setting response headers according to what is defined in this download + * stream ({@link #getContentType()}, {@link #getCacheTime()}, + * {@link #getFileName()}) and transferring the data from the stream ( + * {@link #getStream()}) to the response. Defined parameters ( + * {@link #getParameterNames()}) are also included as headers in the + * response. If there's is a parameter named <code>Location</code>, a + * redirect (302 Moved temporarily) is sent instead of the contents of this + * stream. + * + * @param response + * the wrapped response to write this download stream to + * @throws IOException + * passed through from the wrapped response + * + * @since 7.0 + */ + public void writeTo(WrappedResponse response) throws IOException { + if (getParameter("Location") != null) { + response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + response.setHeader("Location", getParameter("Location")); + return; + } + + // Download from given stream + final InputStream data = getStream(); + if (data != null) { + + OutputStream out = null; + try { + // Sets content type + response.setContentType(getContentType()); + + // Sets cache headers + response.setCacheTime(getCacheTime()); + + // Copy download stream parameters directly + // to HTTP headers. + final Iterator<String> i = getParameterNames(); + if (i != null) { + while (i.hasNext()) { + final String param = i.next(); + response.setHeader(param, getParameter(param)); + } + } + + // suggest local filename from DownloadStream if + // Content-Disposition + // not explicitly set + String contentDispositionValue = getParameter("Content-Disposition"); + if (contentDispositionValue == null) { + contentDispositionValue = "filename=\"" + getFileName() + + "\""; + response.setHeader("Content-Disposition", + contentDispositionValue); + } + + int bufferSize = getBufferSize(); + if (bufferSize <= 0 || bufferSize > Constants.MAX_BUFFER_SIZE) { + bufferSize = Constants.DEFAULT_BUFFER_SIZE; + } + final byte[] buffer = new byte[bufferSize]; + int bytesRead = 0; + + out = response.getOutputStream(); + + long totalWritten = 0; + while ((bytesRead = data.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + + totalWritten += bytesRead; + if (totalWritten >= buffer.length) { + // Avoid chunked encoding for small resources + out.flush(); + } + } + } finally { + tryToCloseStream(out); + tryToCloseStream(data); + } + } + } + + /** + * Helper method that tries to close an output stream and ignores any + * exceptions. + * + * @param out + * the output stream to close, <code>null</code> is also + * supported + */ + static void tryToCloseStream(OutputStream out) { + try { + // try to close output stream (e.g. file handle) + if (out != null) { + out.close(); + } + } catch (IOException e1) { + // NOP + } + } + + /** + * Helper method that tries to close an input stream and ignores any + * exceptions. + * + * @param in + * the input stream to close, <code>null</code> is also supported + */ + static void tryToCloseStream(InputStream in) { + try { + // try to close output stream (e.g. file handle) + if (in != null) { + in.close(); + } + } catch (IOException e1) { + // NOP + } + } + +} diff --git a/server/src/com/vaadin/terminal/ErrorMessage.java b/server/src/com/vaadin/terminal/ErrorMessage.java new file mode 100644 index 0000000000..60a0780a72 --- /dev/null +++ b/server/src/com/vaadin/terminal/ErrorMessage.java @@ -0,0 +1,126 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface for rendering error messages to terminal. All the visible errors + * shown to user must implement this interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ErrorMessage extends Serializable { + + public enum ErrorLevel { + /** + * Error code for informational messages. + */ + INFORMATION("info", 0), + /** + * Error code for warning messages. + */ + WARNING("warning", 1), + /** + * Error code for regular error messages. + */ + ERROR("error", 2), + /** + * Error code for critical error messages. + */ + CRITICAL("critical", 3), + /** + * Error code for system errors and bugs. + */ + SYSTEMERROR("system", 4); + + String text; + int errorLevel; + + private ErrorLevel(String text, int errorLevel) { + this.text = text; + this.errorLevel = errorLevel; + } + + /** + * Textual representation for server-client communication of level + * + * @return String for error severity + */ + public String getText() { + return text; + } + + /** + * Integer representation of error severity for comparison + * + * @return integer for error severity + */ + public int intValue() { + return errorLevel; + } + + @Override + public String toString() { + return text; + } + + } + + /** + * @deprecated from 7.0, use {@link ErrorLevel#SYSTEMERROR} instead   + */ + @Deprecated + public static final ErrorLevel SYSTEMERROR = ErrorLevel.SYSTEMERROR; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#CRITICAL} instead   + */ + @Deprecated + public static final ErrorLevel CRITICAL = ErrorLevel.CRITICAL; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#ERROR} instead   + */ + + @Deprecated + public static final ErrorLevel ERROR = ErrorLevel.ERROR; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#WARNING} instead   + */ + @Deprecated + public static final ErrorLevel WARNING = ErrorLevel.WARNING; + + /** + * @deprecated from 7.0, use {@link ErrorLevel#INFORMATION} instead   + */ + @Deprecated + public static final ErrorLevel INFORMATION = ErrorLevel.INFORMATION; + + /** + * Gets the errors level. + * + * @return the level of error as an integer. + */ + public ErrorLevel getErrorLevel(); + + /** + * Returns the HTML formatted message to show in as the error message on the + * client. + * + * This method should perform any necessary escaping to avoid XSS attacks. + * + * TODO this API may still change to use a separate data transfer object + * + * @return HTML formatted string for the error message + * @since 7.0 + */ + public String getFormattedHtmlMessage(); + +} diff --git a/server/src/com/vaadin/terminal/Extension.java b/server/src/com/vaadin/terminal/Extension.java new file mode 100644 index 0000000000..ef5bb4cf8d --- /dev/null +++ b/server/src/com/vaadin/terminal/Extension.java @@ -0,0 +1,27 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * An extension is an entity that is attached to a Component or another + * Extension and independently communicates between client and server. + * <p> + * An extension can only be attached once. It is not supported to move an + * extension from one target to another. + * <p> + * Extensions can use shared state and RPC in the same way as components. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public interface Extension extends ClientConnector { + /* + * Currently just an empty marker interface to distinguish between + * extensions and other connectors, e.g. components + */ +} diff --git a/server/src/com/vaadin/terminal/ExternalResource.java b/server/src/com/vaadin/terminal/ExternalResource.java new file mode 100644 index 0000000000..84fcc65a44 --- /dev/null +++ b/server/src/com/vaadin/terminal/ExternalResource.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.net.URL; + +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ExternalResource</code> implements source for resources fetched from + * location specified by URL:s. The resources are fetched directly by the client + * terminal and are not fetched trough the terminal adapter. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ExternalResource implements Resource, Serializable { + + /** + * Url of the download. + */ + private String sourceURL = null; + + /** + * MIME Type for the resource + */ + private String mimeType = null; + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + */ + public ExternalResource(URL sourceURL) { + if (sourceURL == null) { + throw new RuntimeException("Source must be non-null"); + } + + this.sourceURL = sourceURL.toString(); + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + * @param mimeType + * the MIME Type + */ + public ExternalResource(URL sourceURL, String mimeType) { + this(sourceURL); + this.mimeType = mimeType; + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + */ + public ExternalResource(String sourceURL) { + if (sourceURL == null) { + throw new RuntimeException("Source must be non-null"); + } + + this.sourceURL = sourceURL.toString(); + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + * @param mimeType + * the MIME Type + */ + public ExternalResource(String sourceURL, String mimeType) { + this(sourceURL); + this.mimeType = mimeType; + } + + /** + * Gets the URL of the external resource. + * + * @return the URL of the external resource. + */ + public String getURL() { + return sourceURL; + } + + /** + * Gets the MIME type of the resource. + * + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + if (mimeType == null) { + mimeType = FileTypeResolver.getMIMEType(getURL().toString()); + } + return mimeType; + } + + /** + * Sets the MIME type of the resource. + */ + public void setMIMEType(String mimeType) { + this.mimeType = mimeType; + } + +} diff --git a/server/src/com/vaadin/terminal/FileResource.java b/server/src/com/vaadin/terminal/FileResource.java new file mode 100644 index 0000000000..e3c9f0172a --- /dev/null +++ b/server/src/com/vaadin/terminal/FileResource.java @@ -0,0 +1,174 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; +import com.vaadin.terminal.Terminal.ErrorEvent; + +/** + * <code>FileResources</code> are files or directories on local filesystem. The + * files and directories are served through URI:s to the client terminal and + * thus must be registered to an URI context before they can be used. The + * resource is automatically registered to the application when it is created. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class FileResource implements ApplicationResource { + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * File where the downloaded content is fetched from. + */ + private File sourceFile; + + /** + * Application. + */ + private final Application application; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DownloadStream.DEFAULT_CACHETIME; + + /** + * Creates a new file resource for providing given file for client + * terminals. + */ + public FileResource(File sourceFile, Application application) { + this.application = application; + setSourceFile(sourceFile); + application.addResource(this); + } + + /** + * Gets the resource as stream. + * + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + @Override + public DownloadStream getStream() { + try { + final DownloadStream ds = new DownloadStream(new FileInputStream( + sourceFile), getMIMEType(), getFilename()); + ds.setParameter("Content-Length", + String.valueOf(sourceFile.length())); + + ds.setCacheTime(cacheTime); + return ds; + } catch (final FileNotFoundException e) { + // Log the exception using the application error handler + getApplication().getErrorHandler().terminalError(new ErrorEvent() { + + @Override + public Throwable getThrowable() { + return e; + } + + }); + + return null; + } + } + + /** + * Gets the source file. + * + * @return the source File. + */ + public File getSourceFile() { + return sourceFile; + } + + /** + * Sets the source file. + * + * @param sourceFile + * the source file to set. + */ + public void setSourceFile(File sourceFile) { + this.sourceFile = sourceFile; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + @Override + public Application getApplication() { + return application; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getFilename() + */ + @Override + public String getFilename() { + return sourceFile.getName(); + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + return FileTypeResolver.getMIMEType(sourceFile); + } + + /** + * Gets the length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Default is + * <code>DownloadStream.DEFAULT_CACHETIME</code>. + * + * @return Cache time in milliseconds. + */ + @Override + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Zero or negavive + * value disbales the caching of this stream. + * + * @param cacheTime + * the cache time in milliseconds. + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + + /* documented in superclass */ + @Override + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + +} diff --git a/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java b/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java new file mode 100644 index 0000000000..265e578c6d --- /dev/null +++ b/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java @@ -0,0 +1,116 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.shared.JavaScriptConnectorState; +import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.AbstractJavaScriptComponent; +import com.vaadin.ui.JavaScript.JavaScriptCallbackRpc; +import com.vaadin.ui.JavaScriptFunction; + +/** + * Internal helper class used to implement functionality common to + * {@link AbstractJavaScriptComponent} and {@link AbstractJavaScriptExtension}. + * Corresponding support in client-side code is in + * {@link JavaScriptConnectorHelper}. + * <p> + * You should most likely no use this class directly. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public class JavaScriptCallbackHelper implements Serializable { + + private static final Method CALL_METHOD = ReflectTools.findMethod( + JavaScriptCallbackRpc.class, "call", String.class, JSONArray.class); + private AbstractClientConnector connector; + + private Map<String, JavaScriptFunction> callbacks = new HashMap<String, JavaScriptFunction>(); + private JavaScriptCallbackRpc javascriptCallbackRpc; + + public JavaScriptCallbackHelper(AbstractClientConnector connector) { + this.connector = connector; + } + + public void registerCallback(String functionName, + JavaScriptFunction javaScriptCallback) { + callbacks.put(functionName, javaScriptCallback); + JavaScriptConnectorState state = getConnectorState(); + if (state.getCallbackNames().add(functionName)) { + connector.requestRepaint(); + } + ensureRpc(); + } + + private JavaScriptConnectorState getConnectorState() { + JavaScriptConnectorState state = (JavaScriptConnectorState) connector + .getState(); + return state; + } + + private void ensureRpc() { + if (javascriptCallbackRpc == null) { + javascriptCallbackRpc = new JavaScriptCallbackRpc() { + @Override + public void call(String name, JSONArray arguments) { + JavaScriptFunction callback = callbacks.get(name); + try { + callback.call(arguments); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + }; + connector.registerRpc(javascriptCallbackRpc); + } + } + + public void invokeCallback(String name, Object... arguments) { + if (callbacks.containsKey(name)) { + throw new IllegalStateException( + "Can't call callback " + + name + + " on the client because a callback with the same name is registered on the server."); + } + JSONArray args = new JSONArray(Arrays.asList(arguments)); + connector.addMethodInvocationToQueue( + JavaScriptCallbackRpc.class.getName(), CALL_METHOD, + new Object[] { name, args }); + connector.requestRepaint(); + } + + public void registerRpc(Class<?> rpcInterfaceType) { + if (rpcInterfaceType == JavaScriptCallbackRpc.class) { + // Ignore + return; + } + Map<String, Set<String>> rpcInterfaces = getConnectorState() + .getRpcInterfaces(); + String interfaceName = rpcInterfaceType.getName(); + if (!rpcInterfaces.containsKey(interfaceName)) { + Set<String> methodNames = new HashSet<String>(); + + for (Method method : rpcInterfaceType.getMethods()) { + methodNames.add(method.getName()); + } + + rpcInterfaces.put(interfaceName, methodNames); + connector.requestRepaint(); + } + } + +} diff --git a/server/src/com/vaadin/terminal/KeyMapper.java b/server/src/com/vaadin/terminal/KeyMapper.java new file mode 100644 index 0000000000..3f19692ef1 --- /dev/null +++ b/server/src/com/vaadin/terminal/KeyMapper.java @@ -0,0 +1,86 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.HashMap; + +/** + * <code>KeyMapper</code> is the simple two-way map for generating textual keys + * for objects and retrieving the objects later with the key. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public class KeyMapper<V> implements Serializable { + + private int lastKey = 0; + + private final HashMap<V, String> objectKeyMap = new HashMap<V, String>(); + + private final HashMap<String, V> keyObjectMap = new HashMap<String, V>(); + + /** + * Gets key for an object. + * + * @param o + * the object. + */ + public String key(V o) { + + if (o == null) { + return "null"; + } + + // If the object is already mapped, use existing key + String key = objectKeyMap.get(o); + if (key != null) { + return key; + } + + // If the object is not yet mapped, map it + key = String.valueOf(++lastKey); + objectKeyMap.put(o, key); + keyObjectMap.put(key, o); + + return key; + } + + /** + * Retrieves object with the key. + * + * @param key + * the name with the desired value. + * @return the object with the key. + */ + public V get(String key) { + return keyObjectMap.get(key); + } + + /** + * Removes object from the mapper. + * + * @param removeobj + * the object to be removed. + */ + public void remove(V removeobj) { + final String key = objectKeyMap.get(removeobj); + + if (key != null) { + objectKeyMap.remove(removeobj); + keyObjectMap.remove(key); + } + } + + /** + * Removes all objects from the mapper. + */ + public void removeAll() { + objectKeyMap.clear(); + keyObjectMap.clear(); + } +} diff --git a/server/src/com/vaadin/terminal/LegacyPaint.java b/server/src/com/vaadin/terminal/LegacyPaint.java new file mode 100644 index 0000000000..ea93e3db7f --- /dev/null +++ b/server/src/com/vaadin/terminal/LegacyPaint.java @@ -0,0 +1,85 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.terminal.PaintTarget.PaintStatus; +import com.vaadin.ui.Component; +import com.vaadin.ui.HasComponents; + +public class LegacyPaint implements Serializable { + /** + * + * <p> + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + * </p> + * + * <p> + * It is called when the contents of the component should be painted in + * response to the component first being shown or having been altered so + * that its visual representation is changed. + * </p> + * + * <p> + * <b>Do not override this to paint your component.</b> Override + * {@link #paintContent(PaintTarget)} instead. + * </p> + * + * + * @param target + * the target UIDL stream where the component should paint itself + * to. + * @throws PaintException + * if the paint operation failed. + */ + public static void paint(Component component, PaintTarget target) + throws PaintException { + // Only paint content of visible components. + if (!isVisibleInContext(component)) { + return; + } + + final String tag = target.getTag(component); + final PaintStatus status = target.startPaintable(component, tag); + if (PaintStatus.CACHED == status) { + // nothing to do but flag as cached and close the paintable tag + target.addAttribute("cached", true); + } else { + // Paint the contents of the component + if (component instanceof Vaadin6Component) { + ((Vaadin6Component) component).paintContent(target); + } + + } + target.endPaintable(component); + + } + + /** + * Checks if the component is visible and its parent is visible, + * recursively. + * <p> + * This is only a helper until paint is moved away from this class. + * + * @return + */ + protected static boolean isVisibleInContext(Component c) { + HasComponents p = c.getParent(); + while (p != null) { + if (!p.isVisible()) { + return false; + } + p = p.getParent(); + } + if (c.getParent() != null && !c.getParent().isComponentVisible(c)) { + return false; + } + + // All parents visible, return this state + return c.isVisible(); + } + +} diff --git a/server/src/com/vaadin/terminal/Page.java b/server/src/com/vaadin/terminal/Page.java new file mode 100644 index 0000000000..a068e7573e --- /dev/null +++ b/server/src/com/vaadin/terminal/Page.java @@ -0,0 +1,646 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.EventObject; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.event.EventRouter; +import com.vaadin.shared.ui.root.PageClientRpc; +import com.vaadin.terminal.WrappedRequest.BrowserDetails; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; +import com.vaadin.terminal.gwt.server.WebApplicationContext; +import com.vaadin.terminal.gwt.server.WebBrowser; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.JavaScript; +import com.vaadin.ui.Notification; +import com.vaadin.ui.Root; + +public class Page implements Serializable { + + /** + * Listener that gets notified when the size of the browser window + * containing the root has changed. + * + * @see Root#addListener(BrowserWindowResizeListener) + */ + public interface BrowserWindowResizeListener extends Serializable { + /** + * Invoked when the browser window containing a Root has been resized. + * + * @param event + * a browser window resize event + */ + public void browserWindowResized(BrowserWindowResizeEvent event); + } + + /** + * Event that is fired when a browser window containing a root is resized. + */ + public class BrowserWindowResizeEvent extends EventObject { + + private final int width; + private final int height; + + /** + * Creates a new event + * + * @param source + * the root for which the browser window has been resized + * @param width + * the new width of the browser window + * @param height + * the new height of the browser window + */ + public BrowserWindowResizeEvent(Page source, int width, int height) { + super(source); + this.width = width; + this.height = height; + } + + @Override + public Page getSource() { + return (Page) super.getSource(); + } + + /** + * Gets the new browser window height + * + * @return an integer with the new pixel height of the browser window + */ + public int getHeight() { + return height; + } + + /** + * Gets the new browser window width + * + * @return an integer with the new pixel width of the browser window + */ + public int getWidth() { + return width; + } + } + + /** + * Private class for storing properties related to opening resources. + */ + private class OpenResource implements Serializable { + + /** + * The resource to open + */ + private final Resource resource; + + /** + * The name of the target window + */ + private final String name; + + /** + * The width of the target window + */ + private final int width; + + /** + * The height of the target window + */ + private final int height; + + /** + * The border style of the target window + */ + private final int border; + + /** + * Creates a new open resource. + * + * @param resource + * The resource to open + * @param name + * The name of the target window + * @param width + * The width of the target window + * @param height + * The height of the target window + * @param border + * The border style of the target window + */ + private OpenResource(Resource resource, String name, int width, + int height, int border) { + this.resource = resource; + this.name = name; + this.width = width; + this.height = height; + this.border = border; + } + + /** + * Paints the open request. Should be painted inside the window. + * + * @param target + * the paint target + * @throws PaintException + * if the paint operation fails + */ + private void paintContent(PaintTarget target) throws PaintException { + target.startTag("open"); + target.addAttribute("src", resource); + if (name != null && name.length() > 0) { + target.addAttribute("name", name); + } + if (width >= 0) { + target.addAttribute("width", width); + } + if (height >= 0) { + target.addAttribute("height", height); + } + switch (border) { + case BORDER_MINIMAL: + target.addAttribute("border", "minimal"); + break; + case BORDER_NONE: + target.addAttribute("border", "none"); + break; + } + + target.endTag("open"); + } + } + + private static final Method BROWSWER_RESIZE_METHOD = ReflectTools + .findMethod(BrowserWindowResizeListener.class, + "browserWindowResized", BrowserWindowResizeEvent.class); + + /** + * A border style used for opening resources in a window without a border. + */ + public static final int BORDER_NONE = 0; + + /** + * A border style used for opening resources in a window with a minimal + * border. + */ + public static final int BORDER_MINIMAL = 1; + + /** + * A border style that indicates that the default border style should be + * used when opening resources. + */ + public static final int BORDER_DEFAULT = 2; + + /** + * Listener that listens changes in URI fragment. + */ + public interface FragmentChangedListener extends Serializable { + public void fragmentChanged(FragmentChangedEvent event); + } + + private static final Method FRAGMENT_CHANGED_METHOD = ReflectTools + .findMethod(Page.FragmentChangedListener.class, "fragmentChanged", + FragmentChangedEvent.class); + + /** + * Resources to be opened automatically on next repaint. The list is + * automatically cleared when it has been sent to the client. + */ + private final LinkedList<OpenResource> openList = new LinkedList<OpenResource>(); + + /** + * A list of notifications that are waiting to be sent to the client. + * Cleared (set to null) when the notifications have been sent. + */ + private List<Notification> notifications; + + /** + * Event fired when uri fragment changes. + */ + public class FragmentChangedEvent extends EventObject { + + /** + * The new uri fragment + */ + private final String fragment; + + /** + * Creates a new instance of UriFragmentReader change event. + * + * @param source + * the Source of the event. + */ + public FragmentChangedEvent(Page source, String fragment) { + super(source); + this.fragment = fragment; + } + + /** + * Gets the root in which the fragment has changed. + * + * @return the root in which the fragment has changed + */ + public Page getPage() { + return (Page) getSource(); + } + + /** + * Get the new fragment + * + * @return the new fragment + */ + public String getFragment() { + return fragment; + } + } + + private EventRouter eventRouter; + + /** + * The current URI fragment. + */ + private String fragment; + + private final Root root; + + private int browserWindowWidth = -1; + private int browserWindowHeight = -1; + + private JavaScript javaScript; + + public Page(Root root) { + this.root = root; + } + + private void addListener(Class<?> eventType, Object target, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, target, method); + } + + private void removeListener(Class<?> eventType, Object target, Method method) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, method); + } + } + + public void addListener(Page.FragmentChangedListener listener) { + addListener(FragmentChangedEvent.class, listener, + FRAGMENT_CHANGED_METHOD); + } + + public void removeListener(Page.FragmentChangedListener listener) { + removeListener(FragmentChangedEvent.class, listener, + FRAGMENT_CHANGED_METHOD); + } + + /** + * Sets URI fragment. Optionally fires a {@link FragmentChangedEvent} + * + * @param newFragment + * id of the new fragment + * @param fireEvent + * true to fire event + * @see FragmentChangedEvent + * @see Page.FragmentChangedListener + */ + public void setFragment(String newFragment, boolean fireEvents) { + if (newFragment == null) { + throw new NullPointerException("The fragment may not be null"); + } + if (!newFragment.equals(fragment)) { + fragment = newFragment; + if (fireEvents) { + fireEvent(new FragmentChangedEvent(this, newFragment)); + } + root.requestRepaint(); + } + } + + private void fireEvent(EventObject event) { + if (eventRouter != null) { + eventRouter.fireEvent(event); + } + } + + /** + * Sets URI fragment. This method fires a {@link FragmentChangedEvent} + * + * @param newFragment + * id of the new fragment + * @see FragmentChangedEvent + * @see Page.FragmentChangedListener + */ + public void setFragment(String newFragment) { + setFragment(newFragment, true); + } + + /** + * Gets currently set URI fragment. + * <p> + * To listen changes in fragment, hook a + * {@link Page.FragmentChangedListener}. + * + * @return the current fragment in browser uri or null if not known + */ + public String getFragment() { + return fragment; + } + + public void init(WrappedRequest request) { + BrowserDetails browserDetails = request.getBrowserDetails(); + if (browserDetails != null) { + fragment = browserDetails.getUriFragment(); + } + } + + public WebBrowser getWebBrowser() { + return ((WebApplicationContext) root.getApplication().getContext()) + .getBrowser(); + } + + public void setBrowserWindowSize(Integer width, Integer height) { + boolean fireEvent = false; + + if (width != null) { + int newWidth = width.intValue(); + if (newWidth != browserWindowWidth) { + browserWindowWidth = newWidth; + fireEvent = true; + } + } + + if (height != null) { + int newHeight = height.intValue(); + if (newHeight != browserWindowHeight) { + browserWindowHeight = newHeight; + fireEvent = true; + } + } + + if (fireEvent) { + fireEvent(new BrowserWindowResizeEvent(this, browserWindowWidth, + browserWindowHeight)); + } + + } + + /** + * Adds a new {@link BrowserWindowResizeListener} to this root. The listener + * will be notified whenever the browser window within which this root + * resides is resized. + * + * @param resizeListener + * the listener to add + * + * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent) + * @see #setResizeLazy(boolean) + */ + public void addListener(BrowserWindowResizeListener resizeListener) { + addListener(BrowserWindowResizeEvent.class, resizeListener, + BROWSWER_RESIZE_METHOD); + } + + /** + * Removes a {@link BrowserWindowResizeListener} from this root. The + * listener will no longer be notified when the browser window is resized. + * + * @param resizeListener + * the listener to remove + */ + public void removeListener(BrowserWindowResizeListener resizeListener) { + removeListener(BrowserWindowResizeEvent.class, resizeListener, + BROWSWER_RESIZE_METHOD); + } + + /** + * Gets the last known height of the browser window in which this root + * resides. + * + * @return the browser window height in pixels + */ + public int getBrowserWindowHeight() { + return browserWindowHeight; + } + + /** + * Gets the last known width of the browser window in which this root + * resides. + * + * @return the browser window width in pixels + */ + public int getBrowserWindowWidth() { + return browserWindowWidth; + } + + public JavaScript getJavaScript() { + if (javaScript == null) { + // Create and attach on first use + javaScript = new JavaScript(); + javaScript.extend(root); + } + + return javaScript; + } + + public void paintContent(PaintTarget target) throws PaintException { + if (!openList.isEmpty()) { + for (final Iterator<OpenResource> i = openList.iterator(); i + .hasNext();) { + (i.next()).paintContent(target); + } + openList.clear(); + } + + // Paint notifications + if (notifications != null) { + target.startTag("notifications"); + for (final Iterator<Notification> it = notifications.iterator(); it + .hasNext();) { + final Notification n = it.next(); + target.startTag("notification"); + if (n.getCaption() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_CAPTION, + n.getCaption()); + } + if (n.getDescription() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_MESSAGE, + n.getDescription()); + } + if (n.getIcon() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_ICON, + n.getIcon()); + } + if (!n.isHtmlContentAllowed()) { + target.addAttribute( + VRoot.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED, true); + } + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_POSITION, + n.getPosition()); + target.addAttribute(VNotification.ATTRIBUTE_NOTIFICATION_DELAY, + n.getDelayMsec()); + if (n.getStyleName() != null) { + target.addAttribute( + VNotification.ATTRIBUTE_NOTIFICATION_STYLE, + n.getStyleName()); + } + target.endTag("notification"); + } + target.endTag("notifications"); + notifications = null; + } + + if (fragment != null) { + target.addAttribute(VRoot.FRAGMENT_VARIABLE, fragment); + } + + } + + /** + * Opens the given resource in this root. The contents of this Root is + * replaced by the {@code Resource}. + * + * @param resource + * the resource to show in this root + */ + public void open(Resource resource) { + openList.add(new OpenResource(resource, null, -1, -1, BORDER_DEFAULT)); + root.requestRepaint(); + } + + /** + * Opens the given resource in a window with the given name. + * <p> + * The supplied {@code windowName} is used as the target name in a + * window.open call in the client. This means that special values such as + * "_blank", "_self", "_top", "_parent" have special meaning. An empty or + * <code>null</code> window name is also a special case. + * </p> + * <p> + * "", null and "_self" as {@code windowName} all causes the resource to be + * opened in the current window, replacing any old contents. For + * downloadable content you should avoid "_self" as "_self" causes the + * client to skip rendering of any other changes as it considers them + * irrelevant (the page will be replaced by the resource). This can speed up + * the opening of a resource, but it might also put the client side into an + * inconsistent state if the window content is not completely replaced e.g., + * if the resource is downloaded instead of displayed in the browser. + * </p> + * <p> + * "_blank" as {@code windowName} causes the resource to always be opened in + * a new window or tab (depends on the browser and browser settings). + * </p> + * <p> + * "_top" and "_parent" as {@code windowName} works as specified by the HTML + * standard. + * </p> + * <p> + * Any other {@code windowName} will open the resource in a window with that + * name, either by opening a new window/tab in the browser or by replacing + * the contents of an existing window with that name. + * </p> + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + */ + public void open(Resource resource, String windowName) { + openList.add(new OpenResource(resource, windowName, -1, -1, + BORDER_DEFAULT)); + root.requestRepaint(); + } + + /** + * Opens the given resource in a window with the given size, border and + * name. For more information on the meaning of {@code windowName}, see + * {@link #open(Resource, String)}. + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + * @param width + * the width of the window in pixels + * @param height + * the height of the window in pixels + * @param border + * the border style of the window. See {@link #BORDER_NONE + * Window.BORDER_* constants} + */ + public void open(Resource resource, String windowName, int width, + int height, int border) { + openList.add(new OpenResource(resource, windowName, width, height, + border)); + root.requestRepaint(); + } + + /** + * Internal helper method to actually add a notification. + * + * @param notification + * the notification to add + */ + private void addNotification(Notification notification) { + if (notifications == null) { + notifications = new LinkedList<Notification>(); + } + notifications.add(notification); + root.requestRepaint(); + } + + /** + * Shows a notification message. + * + * @see Notification + * + * @param notification + * The notification message to show + * + * @deprecated Use Notification.show(Page) instead. + */ + @Deprecated + public void showNotification(Notification notification) { + addNotification(notification); + } + + /** + * Gets the Page to which the current root belongs. This is automatically + * defined when processing requests to the server. In other cases, (e.g. + * from background threads), the current root is not automatically defined. + * + * @see Root#getCurrent() + * + * @return the current page instance if available, otherwise + * <code>null</code> + */ + public static Page getCurrent() { + Root currentRoot = Root.getCurrent(); + if (currentRoot == null) { + return null; + } + return currentRoot.getPage(); + } + + /** + * Sets the page title. The page title is displayed by the browser e.g. as + * the title of the browser window or as the title of the tab. + * + * @param title + * the new page title to set + */ + public void setTitle(String title) { + root.getRpcProxy(PageClientRpc.class).setTitle(title); + } + +} diff --git a/server/src/com/vaadin/terminal/PaintException.java b/server/src/com/vaadin/terminal/PaintException.java new file mode 100644 index 0000000000..68f689b7f1 --- /dev/null +++ b/server/src/com/vaadin/terminal/PaintException.java @@ -0,0 +1,54 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.Serializable; + +/** + * <code>PaintExcepection</code> is thrown if painting of a component fails. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class PaintException extends IOException implements Serializable { + + /** + * Constructs an instance of <code>PaintExeception</code> with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public PaintException(String msg) { + super(msg); + } + + /** + * Constructs an instance of <code>PaintExeception</code> with the specified + * detail message and cause. + * + * @param msg + * the detail message. + * @param cause + * the cause + */ + public PaintException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs an instance of <code>PaintExeception</code> from IOException. + * + * @param exception + * the original exception. + */ + public PaintException(IOException exception) { + super(exception.getMessage()); + } +} diff --git a/server/src/com/vaadin/terminal/PaintTarget.java b/server/src/com/vaadin/terminal/PaintTarget.java new file mode 100644 index 0000000000..b658c9f4a3 --- /dev/null +++ b/server/src/com/vaadin/terminal/PaintTarget.java @@ -0,0 +1,509 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Map; + +import com.vaadin.terminal.StreamVariable.StreamingStartEvent; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.ui.Component; + +/** + * This interface defines the methods for painting XML to the UIDL stream. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface PaintTarget extends Serializable { + + /** + * Prints single XMLsection. + * + * Prints full XML section. The section data is escaped from XML tags and + * surrounded by XML start and end-tags. + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the scetion data. + * @throws PaintException + * if the paint operation failed. + */ + public void addSection(String sectionTagName, String sectionData) + throws PaintException; + + /** + * Result of starting to paint a Paintable ( + * {@link PaintTarget#startPaintable(Component, String)}). + * + * @since 7.0 + */ + public enum PaintStatus { + /** + * Painting started, addVariable() and addAttribute() etc. methods may + * be called. + */ + PAINTING, + /** + * A previously unpainted or painted {@link Paintable} has been queued + * be created/update later in a separate change in the same set of + * changes. + */ + CACHED + } + + /** + * Prints element start tag of a paintable section. Starts a paintable + * section using the given tag. The PaintTarget may implement a caching + * scheme, that checks the paintable has actually changed or can a cached + * version be used instead. This method should call the startTag method. + * <p> + * If the Paintable is found in cache and this function returns true it may + * omit the content and close the tag, in which case cached content should + * be used. + * </p> + * <p> + * This method may also add only a reference to the paintable and queue the + * paintable to be painted separately. + * </p> + * <p> + * Each paintable being painted should be closed by a matching + * {@link #endPaintable(Component)} regardless of the {@link PaintStatus} + * returned. + * </p> + * + * @param paintable + * the paintable to start. + * @param tag + * the name of the start tag. + * @return {@link PaintStatus} - ready to paint or already cached on the + * client (also used for sub paintables that are painted later + * separately) + * @throws PaintException + * if the paint operation failed. + * @see #startTag(String) + * @since 7.0 (previously using startTag(Paintable, String)) + */ + public PaintStatus startPaintable(Component paintable, String tag) + throws PaintException; + + /** + * Prints paintable element end tag. + * + * Calls to {@link #startPaintable(Component, String)}should be matched by + * {@link #endPaintable(Component)}. If the parent tag is closed before + * every child tag is closed a PaintException is raised. + * + * @param paintable + * the paintable to close. + * @throws PaintException + * if the paint operation failed. + * @since 7.0 (previously using engTag(String)) + */ + public void endPaintable(Component paintable) throws PaintException; + + /** + * Prints element start tag. + * + * <pre> + * Todo: + * Checking of input values + * </pre> + * + * @param tagName + * the name of the start tag. + * @throws PaintException + * if the paint operation failed. + */ + public void startTag(String tagName) throws PaintException; + + /** + * Prints element end tag. + * + * If the parent tag is closed before every child tag is closed an + * PaintException is raised. + * + * @param tagName + * the name of the end tag. + * @throws PaintException + * if the paint operation failed. + */ + public void endTag(String tagName) throws PaintException; + + /** + * Adds a boolean attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, boolean value) throws PaintException; + + /** + * Adds a integer attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, int value) throws PaintException; + + /** + * Adds a resource attribute to component. Atributes must be added before + * any content is written. + * + * @param name + * the Attribute name + * @param value + * the Attribute value + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, Resource value) throws PaintException; + + /** + * Adds details about {@link StreamVariable} to the UIDL stream. Eg. in web + * terminals Receivers are typically rendered for the client side as URLs, + * where the client side implementation can do an http post request. + * <p> + * The urls in UIDL message may use Vaadin specific protocol. Before + * actually using the urls on the client side, they should be passed via + * {@link ApplicationConnection#translateVaadinUri(String)}. + * <p> + * Note that in current terminal implementation StreamVariables are cleaned + * from the terminal only when: + * <ul> + * <li>a StreamVariable with same name replaces an old one + * <li>the variable owner is no more attached + * <li>the developer signals this by calling + * {@link StreamingStartEvent#disposeStreamVariable()} + * </ul> + * Most commonly a component developer can just ignore this issue, but with + * strict memory requirements and lots of StreamVariables implementations + * that reserve a lot of memory this may be a critical issue. + * + * @param owner + * the ReceiverOwner that can track the progress of streaming to + * the given StreamVariable + * @param name + * an identifying name for the StreamVariable + * @param value + * the StreamVariable to paint + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, + StreamVariable value) throws PaintException; + + /** + * Adds a long attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, long value) throws PaintException; + + /** + * Adds a float attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, float value) throws PaintException; + + /** + * Adds a double attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, double value) throws PaintException; + + /** + * Adds a string attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Boolean attribute name. + * @param value + * the Boolean attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, String value) throws PaintException; + + /** + * TODO + * + * @param name + * @param value + * @throws PaintException + */ + public void addAttribute(String name, Map<?, ?> value) + throws PaintException; + + /** + * Adds a Paintable type attribute. On client side the value will be a + * terminal specific reference to corresponding component on client side + * implementation. + * + * @param name + * the name of the attribute + * @param value + * the Paintable to be referenced on client side + * @throws PaintException + */ + public void addAttribute(String name, Component value) + throws PaintException; + + /** + * Adds a string type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String value) + throws PaintException; + + /** + * Adds a int type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, int value) + throws PaintException; + + /** + * Adds a long type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, long value) + throws PaintException; + + /** + * Adds a float type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, float value) + throws PaintException; + + /** + * Adds a double type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, double value) + throws PaintException; + + /** + * Adds a boolean type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, boolean value) + throws PaintException; + + /** + * Adds a string array type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String[] value) + throws PaintException; + + /** + * Adds a Paintable type variable. On client side the variable value will be + * a terminal specific reference to corresponding component on client side + * implementation. When updated from client side, terminal will map the + * client side component reference back to a corresponding server side + * reference. + * + * @param owner + * the Listener for variable changes + * @param name + * the name of the variable + * @param value + * the initial value of the variable + * + * @throws PaintException + * if the paint oparation fails + */ + public void addVariable(VariableOwner owner, String name, Component value) + throws PaintException; + + /** + * Adds a upload stream type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addUploadStreamVariable(VariableOwner owner, String name) + throws PaintException; + + /** + * Prints single XML section. + * <p> + * Prints full XML section. The section data must be XML and it is + * surrounded by XML start and end-tags. + * </p> + * + * @param sectionTagName + * the tag name. + * @param sectionData + * the section data to be printed. + * @param namespace + * the namespace. + * @throws PaintException + * if the paint operation failed. + */ + public void addXMLSection(String sectionTagName, String sectionData, + String namespace) throws PaintException; + + /** + * Adds UIDL directly. The UIDL must be valid in accordance with the + * UIDL.dtd + * + * @param uidl + * the UIDL to be added. + * @throws PaintException + * if the paint operation failed. + */ + public void addUIDL(java.lang.String uidl) throws PaintException; + + /** + * Adds text node. All the contents of the text are XML-escaped. + * + * @param text + * the Text to add + * @throws PaintException + * if the paint operation failed. + */ + void addText(String text) throws PaintException; + + /** + * Adds CDATA node to target UIDL-tree. + * + * @param text + * the Character data to add + * @throws PaintException + * if the paint operation failed. + * @since 3.1 + */ + void addCharacterData(String text) throws PaintException; + + public void addAttribute(String string, Object[] keys); + + /** + * @return the "tag" string used in communication to present given + * {@link ClientConnector} type. Terminal may define how to present + * the connector. + */ + public String getTag(ClientConnector paintable); + + /** + * @return true if a full repaint has been requested. E.g. refresh in a + * browser window or such. + */ + public boolean isFullRepaint(); + +} diff --git a/server/src/com/vaadin/terminal/RequestHandler.java b/server/src/com/vaadin/terminal/RequestHandler.java new file mode 100644 index 0000000000..f37201715d --- /dev/null +++ b/server/src/com/vaadin/terminal/RequestHandler.java @@ -0,0 +1,36 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.Serializable; + +import com.vaadin.Application; + +/** + * Handler for producing a response to non-UIDL requests. Handlers can be added + * to applications using {@link Application#addRequestHandler(RequestHandler)} + */ +public interface RequestHandler extends Serializable { + + /** + * Handles a non-UIDL request. If a response is written, this method should + * return <code>false</code> to indicate that no more request handlers + * should be invoked for the request. + * + * @param application + * The application to which the request belongs + * @param request + * The request to handle + * @param response + * The response object to which a response can be written. + * @return true if a response has been written and no further request + * handlers should be called, otherwise false + * @throws IOException + */ + boolean handleRequest(Application application, WrappedRequest request, + WrappedResponse response) throws IOException; + +} diff --git a/server/src/com/vaadin/terminal/Resource.java b/server/src/com/vaadin/terminal/Resource.java new file mode 100644 index 0000000000..58dc4fea9d --- /dev/null +++ b/server/src/com/vaadin/terminal/Resource.java @@ -0,0 +1,26 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * <code>Resource</code> provided to the client terminal. Support for actually + * displaying the resource type is left to the terminal. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Resource extends Serializable { + + /** + * Gets the MIME type of the resource. + * + * @return the MIME type of the resource. + */ + public String getMIMEType(); +} diff --git a/server/src/com/vaadin/terminal/Scrollable.java b/server/src/com/vaadin/terminal/Scrollable.java new file mode 100644 index 0000000000..472954c556 --- /dev/null +++ b/server/src/com/vaadin/terminal/Scrollable.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * <p> + * This interface is implemented by all visual objects that can be scrolled + * programmatically from the server-side. The unit of scrolling is pixel. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Scrollable extends Serializable { + + /** + * Gets scroll left offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + * </p> + * + * @return Horizontal scrolling position in pixels. + */ + public int getScrollLeft(); + + /** + * Sets scroll left offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + * </p> + * + * @param scrollLeft + * the xOffset. + */ + public void setScrollLeft(int scrollLeft); + + /** + * Gets scroll top offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + * </p> + * + * @return Vertical scrolling position in pixels. + */ + public int getScrollTop(); + + /** + * Sets scroll top offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + * </p> + * + * <p> + * The scrolling position is limited by the current height of the content + * area. If the position is below the height, it is scrolled to the bottom. + * However, if the same response also adds height to the content area, + * scrolling to bottom only scrolls to the bottom of the previous content + * area. + * </p> + * + * @param scrollTop + * the yOffset. + */ + public void setScrollTop(int scrollTop); + +} diff --git a/server/src/com/vaadin/terminal/Sizeable.java b/server/src/com/vaadin/terminal/Sizeable.java new file mode 100644 index 0000000000..e3c98e0fa9 --- /dev/null +++ b/server/src/com/vaadin/terminal/Sizeable.java @@ -0,0 +1,242 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface to be implemented by components wishing to display some object that + * may be dynamically resized during runtime. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Sizeable extends Serializable { + + /** + * @deprecated from 7.0, use {@link Unit#PIXELS} instead   + */ + @Deprecated + public static final Unit UNITS_PIXELS = Unit.PIXELS; + + /** + * @deprecated from 7.0, use {@link Unit#POINTS} instead   + */ + @Deprecated + public static final Unit UNITS_POINTS = Unit.POINTS; + + /** + * @deprecated from 7.0, use {@link Unit#PICAS} instead   + */ + @Deprecated + public static final Unit UNITS_PICAS = Unit.PICAS; + + /** + * @deprecated from 7.0, use {@link Unit#EM} instead   + */ + @Deprecated + public static final Unit UNITS_EM = Unit.EM; + + /** + * @deprecated from 7.0, use {@link Unit#EX} instead   + */ + @Deprecated + public static final Unit UNITS_EX = Unit.EX; + + /** + * @deprecated from 7.0, use {@link Unit#MM} instead   + */ + @Deprecated + public static final Unit UNITS_MM = Unit.MM; + + /** + * @deprecated from 7.0, use {@link Unit#CM} instead   + */ + @Deprecated + public static final Unit UNITS_CM = Unit.CM; + + /** + * @deprecated from 7.0, use {@link Unit#INCH} instead   + */ + @Deprecated + public static final Unit UNITS_INCH = Unit.INCH; + + /** + * @deprecated from 7.0, use {@link Unit#PERCENTAGE} instead   + */ + @Deprecated + public static final Unit UNITS_PERCENTAGE = Unit.PERCENTAGE; + + public static final float SIZE_UNDEFINED = -1; + + public enum Unit { + /** + * Unit code representing pixels. + */ + PIXELS("px"), + /** + * Unit code representing points (1/72nd of an inch). + */ + POINTS("pt"), + /** + * Unit code representing picas (12 points). + */ + PICAS("pc"), + /** + * Unit code representing the font-size of the relevant font. + */ + EM("em"), + /** + * Unit code representing the x-height of the relevant font. + */ + EX("ex"), + /** + * Unit code representing millimeters. + */ + MM("mm"), + /** + * Unit code representing centimeters. + */ + CM("cm"), + /** + * Unit code representing inches. + */ + INCH("in"), + /** + * Unit code representing in percentage of the containing element + * defined by terminal. + */ + PERCENTAGE("%"); + + private String symbol; + + private Unit(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + @Override + public String toString() { + return symbol; + } + + public static Unit getUnitFromSymbol(String symbol) { + if (symbol == null) { + return Unit.PIXELS; // Defaults to pixels + } + for (Unit unit : Unit.values()) { + if (symbol.equals(unit.getSymbol())) { + return unit; + } + } + return Unit.PIXELS; // Defaults to pixels + } + } + + /** + * Gets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @return width of the object in units specified by widthUnits property. + */ + public float getWidth(); + + /** + * Gets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @return height of the object in units specified by heightUnits property. + */ + public float getHeight(); + + /** + * Gets the width property units. + * + * @return units used in width property. + */ + public Unit getWidthUnits(); + + /** + * Gets the height property units. + * + * @return units used in height property. + */ + public Unit getHeightUnits(); + + /** + * Sets the height of the component using String presentation. + * + * String presentation is similar to what is used in Cascading Style Sheets. + * Size can be length or percentage of available size. + * + * The empty string ("") or null will unset the height and set the units to + * pixels. + * + * See <a + * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS + * specification</a> for more details. + * + * @param height + * in CSS style string representation + */ + public void setHeight(String height); + + /** + * Sets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param width + * the width of the object. + * @param unit + * the unit used for the width. + */ + public void setWidth(float width, Unit unit); + + /** + * Sets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param height + * the height of the object. + * @param unit + * the unit used for the width. + */ + public void setHeight(float height, Unit unit); + + /** + * Sets the width of the component using String presentation. + * + * String presentation is similar to what is used in Cascading Style Sheets. + * Size can be length or percentage of available size. + * + * The empty string ("") or null will unset the width and set the units to + * pixels. + * + * See <a + * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS + * specification</a> for more details. + * + * @param width + * in CSS style string representation, null or empty string to + * reset + */ + public void setWidth(String width); + + /** + * Sets the size to 100% x 100%. + */ + public void setSizeFull(); + + /** + * Clears any size settings. + */ + public void setSizeUndefined(); + +} diff --git a/server/src/com/vaadin/terminal/StreamResource.java b/server/src/com/vaadin/terminal/StreamResource.java new file mode 100644 index 0000000000..1afd91dc08 --- /dev/null +++ b/server/src/com/vaadin/terminal/StreamResource.java @@ -0,0 +1,222 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.InputStream; +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>StreamResource</code> is a resource provided to the client directly by + * the application. The strean resource is fetched from URI that is most often + * in the context of the application or window. The resource is automatically + * registered to window in creation. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class StreamResource implements ApplicationResource { + + /** + * Source stream the downloaded content is fetched from. + */ + private StreamSource streamSource = null; + + /** + * Explicit mime-type. + */ + private String MIMEType = null; + + /** + * Filename. + */ + private String filename; + + /** + * Application. + */ + private final Application application; + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DEFAULT_CACHETIME; + + /** + * Creates a new stream resource for downloading from stream. + * + * @param streamSource + * the source Stream. + * @param filename + * the name of the file. + * @param application + * the Application object. + */ + public StreamResource(StreamSource streamSource, String filename, + Application application) { + + this.application = application; + setFilename(filename); + setStreamSource(streamSource); + + // Register to application + application.addResource(this); + + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + if (MIMEType != null) { + return MIMEType; + } + return FileTypeResolver.getMIMEType(filename); + } + + /** + * Sets the mime type of the resource. + * + * @param MIMEType + * the MIME type to be set. + */ + public void setMIMEType(String MIMEType) { + this.MIMEType = MIMEType; + } + + /** + * Returns the source for this <code>StreamResource</code>. StreamSource is + * queried when the resource is about to be streamed to the client. + * + * @return Source of the StreamResource. + */ + public StreamSource getStreamSource() { + return streamSource; + } + + /** + * Sets the source for this <code>StreamResource</code>. + * <code>StreamSource</code> is queried when the resource is about to be + * streamed to the client. + * + * @param streamSource + * the source to set. + */ + public void setStreamSource(StreamSource streamSource) { + this.streamSource = streamSource; + } + + /** + * Gets the filename. + * + * @return the filename. + */ + @Override + public String getFilename() { + return filename; + } + + /** + * Sets the filename. + * + * @param filename + * the filename to set. + */ + public void setFilename(String filename) { + this.filename = filename; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + @Override + public Application getApplication() { + return application; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + @Override + public DownloadStream getStream() { + final StreamSource ss = getStreamSource(); + if (ss == null) { + return null; + } + final DownloadStream ds = new DownloadStream(ss.getStream(), + getMIMEType(), getFilename()); + ds.setBufferSize(getBufferSize()); + ds.setCacheTime(cacheTime); + return ds; + } + + /** + * Interface implemented by the source of a StreamResource. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface StreamSource extends Serializable { + + /** + * Returns new input stream that is used for reading the resource. + */ + public InputStream getStream(); + } + + /* documented in superclass */ + @Override + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /* documented in superclass */ + @Override + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Zero or negavive value disbales the caching of this + * stream. + * </p> + * + * @param cacheTime + * the cache time in milliseconds. + * + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + +} diff --git a/server/src/com/vaadin/terminal/StreamVariable.java b/server/src/com/vaadin/terminal/StreamVariable.java new file mode 100644 index 0000000000..63763a5751 --- /dev/null +++ b/server/src/com/vaadin/terminal/StreamVariable.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.io.OutputStream; +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.terminal.StreamVariable.StreamingEndEvent; +import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; +import com.vaadin.terminal.StreamVariable.StreamingStartEvent; + +/** + * StreamVariable is a special kind of variable whose value is streamed to an + * {@link OutputStream} provided by the {@link #getOutputStream()} method. E.g. + * in web terminals {@link StreamVariable} can be used to send large files from + * browsers to the server without consuming large amounts of memory. + * <p> + * Note, writing to the {@link OutputStream} is not synchronized by the terminal + * (to avoid stalls in other operations when eg. streaming to a slow network + * service or file system). If UI is changed as a side effect of writing to the + * output stream, developer must handle synchronization manually. + * <p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.5 + * @see PaintTarget#addVariable(VariableOwner, String, StreamVariable) + */ +public interface StreamVariable extends Serializable { + + /** + * Invoked by the terminal when a new upload arrives, after + * {@link #streamingStarted(StreamingStartEvent)} method has been called. + * The terminal implementation will write the streamed variable to the + * returned output stream. + * + * @return Stream to which the uploaded file should be written. + */ + public OutputStream getOutputStream(); + + /** + * Whether the {@link #onProgress(long, long)} method should be called + * during the upload. + * <p> + * {@link #onProgress(long, long)} is called in a synchronized block when + * the content is being received. This is potentially bit slow, so we are + * calling that method only if requested. The value is requested after the + * {@link #uploadStarted(StreamingStartEvent)} event, but not after reading + * each buffer. + * + * @return true if this {@link StreamVariable} wants to by notified during + * the upload of the progress of streaming. + * @see #onProgress(StreamingProgressEvent) + */ + public boolean listenProgress(); + + /** + * This method is called by the terminal if {@link #listenProgress()} + * returns true when the streaming starts. + */ + public void onProgress(StreamingProgressEvent event); + + public void streamingStarted(StreamingStartEvent event); + + public void streamingFinished(StreamingEndEvent event); + + public void streamingFailed(StreamingErrorEvent event); + + /* + * Not synchronized to avoid stalls (caused by UIDL requests) while + * streaming the content. Implementations also most commonly atomic even + * without the restriction. + */ + /** + * If this method returns true while the content is being streamed the + * Terminal to stop receiving current upload. + * <p> + * Note, the usage of this method is not synchronized over the Application + * instance by the terminal like other methods. The implementation should + * only return a boolean field and especially not modify UI or implement a + * synchronization by itself. + * + * @return true if the streaming should be interrupted as soon as possible. + */ + public boolean isInterrupted(); + + public interface StreamingEvent extends Serializable { + + /** + * @return the file name of the streamed file if known + */ + public String getFileName(); + + /** + * @return the mime type of the streamed file if known + */ + public String getMimeType(); + + /** + * @return the length of the stream (in bytes) if known, else -1 + */ + public long getContentLength(); + + /** + * @return then number of bytes streamed to StreamVariable + */ + public long getBytesReceived(); + } + + /** + * Event passed to {@link #uploadStarted(StreamingStartEvent)} method before + * the streaming of the content to {@link StreamVariable} starts. + */ + public interface StreamingStartEvent extends StreamingEvent { + /** + * The owner of the StreamVariable can call this method to inform the + * terminal implementation that this StreamVariable will not be used to + * accept more post. + */ + public void disposeStreamVariable(); + } + + /** + * Event passed to {@link #onProgress(StreamingProgressEvent)} method during + * the streaming progresses. + */ + public interface StreamingProgressEvent extends StreamingEvent { + } + + /** + * Event passed to {@link #uploadFinished(StreamingEndEvent)} method the + * contents have been streamed to StreamVariable successfully. + */ + public interface StreamingEndEvent extends StreamingEvent { + } + + /** + * Event passed to {@link #uploadFailed(StreamingErrorEvent)} method when + * the streaming ended before the end of the input. The streaming may fail + * due an interruption by {@link } or due an other unknown exception in + * communication. In the latter case the exception is also passed to + * {@link Application#terminalError(com.vaadin.terminal.Terminal.ErrorEvent)} + * . + */ + public interface StreamingErrorEvent extends StreamingEvent { + + /** + * @return the exception that caused the receiving not to finish cleanly + */ + public Exception getException(); + + } + +} diff --git a/server/src/com/vaadin/terminal/SystemError.java b/server/src/com/vaadin/terminal/SystemError.java new file mode 100644 index 0000000000..bae135ee6b --- /dev/null +++ b/server/src/com/vaadin/terminal/SystemError.java @@ -0,0 +1,82 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; + +/** + * <code>SystemError</code> is an error message for a problem caused by error in + * system, not the user application code. The system error can contain technical + * information such as stack trace and exception. + * + * SystemError does not support HTML in error messages or stack traces. If HTML + * messages are required, use {@link UserError} or a custom implementation of + * {@link ErrorMessage}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class SystemError extends AbstractErrorMessage { + + /** + * Constructor for SystemError with error message specified. + * + * @param message + * the Textual error description. + */ + public SystemError(String message) { + super(message); + setErrorLevel(ErrorLevel.SYSTEMERROR); + setMode(ContentMode.XHTML); + setMessage(getHtmlMessage()); + } + + /** + * Constructor for SystemError with causing exception and error message. + * + * @param message + * the Textual error description. + * @param cause + * the throwable causing the system error. + */ + public SystemError(String message, Throwable cause) { + this(message); + addCause(AbstractErrorMessage.getErrorMessageForException(cause)); + } + + /** + * Constructor for SystemError with cause. + * + * @param cause + * the throwable causing the system error. + */ + public SystemError(Throwable cause) { + this(null, cause); + } + + /** + * Returns the message of the error in HTML. + * + * Note that this API may change in future versions. + */ + protected String getHtmlMessage() { + // TODO wrapping div with namespace? See the old code: + // target.addXMLSection("div", message, + // "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"); + + StringBuilder sb = new StringBuilder(); + if (getMessage() != null) { + sb.append("<h2>"); + sb.append(AbstractApplicationServlet + .safeEscapeForHtml(getMessage())); + sb.append("</h2>"); + } + return sb.toString(); + } + +} diff --git a/server/src/com/vaadin/terminal/Terminal.java b/server/src/com/vaadin/terminal/Terminal.java new file mode 100644 index 0000000000..9dc6ced6a7 --- /dev/null +++ b/server/src/com/vaadin/terminal/Terminal.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * An interface that provides information about the user's terminal. + * Implementors typically provide additional information using methods not in + * this interface. </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Terminal extends Serializable { + + /** + * Gets the name of the default theme for this terminal. + * + * @return the name of the theme that is used by default by this terminal. + */ + public String getDefaultTheme(); + + /** + * Gets the width of the terminal screen in pixels. This is the width of the + * screen and not the width available for the application. + * <p> + * Note that the screen width is typically not available in the + * {@link com.vaadin.Application#init()} method as this is called before the + * browser has a chance to report the screen size to the server. + * </p> + * + * @return the width of the terminal screen. + */ + public int getScreenWidth(); + + /** + * Gets the height of the terminal screen in pixels. This is the height of + * the screen and not the height available for the application. + * + * <p> + * Note that the screen height is typically not available in the + * {@link com.vaadin.Application#init()} method as this is called before the + * browser has a chance to report the screen size to the server. + * </p> + * + * @return the height of the terminal screen. + */ + public int getScreenHeight(); + + /** + * An error event implementation for Terminal. + */ + public interface ErrorEvent extends Serializable { + + /** + * Gets the contained throwable, the cause of the error. + */ + public Throwable getThrowable(); + + } + + /** + * Interface for listening to Terminal errors. + */ + public interface ErrorListener extends Serializable { + + /** + * Invoked when a terminal error occurs. + * + * @param event + * the fired event. + */ + public void terminalError(Terminal.ErrorEvent event); + } +} diff --git a/server/src/com/vaadin/terminal/ThemeResource.java b/server/src/com/vaadin/terminal/ThemeResource.java new file mode 100644 index 0000000000..41674b2373 --- /dev/null +++ b/server/src/com/vaadin/terminal/ThemeResource.java @@ -0,0 +1,96 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ThemeResource</code> is a named theme dependant resource provided and + * managed by a theme. The actual resource contents are dynamically resolved to + * comply with the used theme by the terminal adapter. This is commonly used to + * provide static images, flash, java-applets, etc for the terminals. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ThemeResource implements Resource { + + /** + * Id of the terminal managed resource. + */ + private String resourceID = null; + + /** + * Creates a resource. + * + * @param resourceId + * the Id of the resource. + */ + public ThemeResource(String resourceId) { + if (resourceId == null) { + throw new NullPointerException("Resource ID must not be null"); + } + if (resourceId.length() == 0) { + throw new IllegalArgumentException("Resource ID can not be empty"); + } + if (resourceId.charAt(0) == '/') { + throw new IllegalArgumentException( + "Resource ID must be relative (can not begin with /)"); + } + + resourceID = resourceId; + } + + /** + * Tests if the given object equals this Resource. + * + * @param obj + * the object to be tested for equality. + * @return <code>true</code> if the given object equals this Icon, + * <code>false</code> if not. + * @see java.lang.Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + return obj instanceof ThemeResource + && resourceID.equals(((ThemeResource) obj).resourceID); + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return resourceID.hashCode(); + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return resourceID.toString(); + } + + /** + * Gets the resource id. + * + * @return the resource id. + */ + public String getResourceId() { + return resourceID; + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + @Override + public String getMIMEType() { + return FileTypeResolver.getMIMEType(getResourceId()); + } +} diff --git a/server/src/com/vaadin/terminal/UserError.java b/server/src/com/vaadin/terminal/UserError.java new file mode 100644 index 0000000000..a7a4fd89e2 --- /dev/null +++ b/server/src/com/vaadin/terminal/UserError.java @@ -0,0 +1,70 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +/** + * <code>UserError</code> is a controlled error occurred in application. User + * errors are occur in normal usage of the application and guide the user. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class UserError extends AbstractErrorMessage { + + /** + * @deprecated from 7.0, use {@link ContentMode#TEXT} instead   + */ + @Deprecated + public static final ContentMode CONTENT_TEXT = ContentMode.TEXT; + + /** + * @deprecated from 7.0, use {@link ContentMode#PREFORMATTED} instead   + */ + @Deprecated + public static final ContentMode CONTENT_PREFORMATTED = ContentMode.PREFORMATTED; + + /** + * @deprecated from 7.0, use {@link ContentMode#XHTML} instead   + */ + @Deprecated + public static final ContentMode CONTENT_XHTML = ContentMode.XHTML; + + /** + * Creates a textual error message of level ERROR. + * + * @param textErrorMessage + * the text of the error message. + */ + public UserError(String textErrorMessage) { + super(textErrorMessage); + } + + /** + * Creates an error message with level and content mode. + * + * @param message + * the error message. + * @param contentMode + * the content Mode. + * @param errorLevel + * the level of error. + */ + public UserError(String message, ContentMode contentMode, + ErrorLevel errorLevel) { + super(message); + if (contentMode == null) { + contentMode = ContentMode.TEXT; + } + if (errorLevel == null) { + errorLevel = ErrorLevel.ERROR; + } + setMode(contentMode); + setErrorLevel(errorLevel); + } + +} diff --git a/server/src/com/vaadin/terminal/Vaadin6Component.java b/server/src/com/vaadin/terminal/Vaadin6Component.java new file mode 100644 index 0000000000..59cbf956ca --- /dev/null +++ b/server/src/com/vaadin/terminal/Vaadin6Component.java @@ -0,0 +1,44 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal; + +import java.util.EventListener; + +import com.vaadin.ui.Component; + +/** + * Interface provided to ease porting of Vaadin 6 components to Vaadin 7. By + * implementing this interface your Component will be able to use + * {@link #paintContent(PaintTarget)} and + * {@link #changeVariables(Object, java.util.Map)} just like in Vaadin 6. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public interface Vaadin6Component extends VariableOwner, Component, + EventListener { + + /** + * <p> + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + * </p> + * + * <p> + * It is called when the contents of the component should be painted in + * response to the component first being shown or having been altered so + * that its visual representation is changed. + * </p> + * + * @param target + * the target UIDL stream where the component should paint itself + * to. + * @throws PaintException + * if the paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException; + +} diff --git a/server/src/com/vaadin/terminal/VariableOwner.java b/server/src/com/vaadin/terminal/VariableOwner.java new file mode 100644 index 0000000000..c52e04c008 --- /dev/null +++ b/server/src/com/vaadin/terminal/VariableOwner.java @@ -0,0 +1,85 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Map; + +/** + * <p> + * Listener interface for UI variable changes. The user communicates with the + * application using the so-called <i>variables</i>. When the user makes a + * change using the UI the terminal trasmits the changed variables to the + * application, and the components owning those variables may then process those + * changes. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @deprecated in 7.0. Only provided to ease porting of Vaadin 6 components. Do + * not implement this directly, implement {@link Vaadin6Component}. + */ +@Deprecated +public interface VariableOwner extends Serializable { + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @param source + * the Source of the variable change. This is the origin of the + * event. For example in Web Adapter this is the request. + * @param variables + * the Mapping from variable names to new variable values. + */ + public void changeVariables(Object source, Map<String, Object> variables); + + /** + * <p> + * Tests if the variable owner is enabled or not. The terminal should not + * send any variable changes to disabled variable owners. + * </p> + * + * @return <code>true</code> if the variable owner is enabled, + * <code>false</code> if not + */ + public boolean isEnabled(); + + /** + * <p> + * Tests if the variable owner is in immediate mode or not. Being in + * immediate mode means that all variable changes are required to be sent + * back from the terminal immediately when they occur. + * </p> + * + * <p> + * <strong>Note:</strong> <code>VariableOwner</code> does not include a set- + * method for the immediateness property. This is because not all + * VariableOwners wish to offer the functionality. Such VariableOwners are + * never in the immediate mode, thus they always return <code>false</code> + * in {@link #isImmediate()}. + * </p> + * + * @return <code>true</code> if the component is in immediate mode, + * <code>false</code> if not. + */ + public boolean isImmediate(); + + /** + * VariableOwner error event. + */ + public interface ErrorEvent extends Terminal.ErrorEvent { + + /** + * Gets the source VariableOwner. + * + * @return the variable owner. + */ + public VariableOwner getVariableOwner(); + + } +} diff --git a/server/src/com/vaadin/terminal/WrappedRequest.java b/server/src/com/vaadin/terminal/WrappedRequest.java new file mode 100644 index 0000000000..a27213d921 --- /dev/null +++ b/server/src/com/vaadin/terminal/WrappedRequest.java @@ -0,0 +1,277 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.Locale; +import java.util.Map; + +import javax.portlet.PortletRequest; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import com.vaadin.Application; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.annotations.EagerInit; +import com.vaadin.terminal.gwt.server.WebBrowser; +import com.vaadin.ui.Root; + +/** + * A generic request to the server, wrapping a more specific request type, e.g. + * HttpServletReqest or PortletRequest. + * + * @since 7.0 + */ +public interface WrappedRequest extends Serializable { + + /** + * Detailed information extracted from the browser. + * + * @see WrappedRequest#getBrowserDetails() + */ + public interface BrowserDetails extends Serializable { + /** + * Gets the URI hash fragment for the request. This is typically used to + * encode navigation within an application. + * + * @return the URI hash fragment + */ + public String getUriFragment(); + + /** + * Gets the value of window.name from the browser. This can be used to + * keep track of the specific window between browser reloads. + * + * @return the string value of window.name in the browser + */ + public String getWindowName(); + + /** + * Gets a reference to the {@link WebBrowser} object containing + * additional information, e.g. screen size and the time zone offset. + * + * @return the web browser object + */ + public WebBrowser getWebBrowser(); + } + + /** + * Gets the named request parameter This is typically a HTTP GET or POST + * parameter, though other request types might have other ways of + * representing parameters. + * + * @see javax.servlet.ServletRequest#getParameter(String) + * @see javax.portlet.PortletRequest#getParameter(String) + * + * @param parameter + * the name of the parameter + * @return The paramter value, or <code>null</code> if no parameter with the + * given name is present + */ + public String getParameter(String parameter); + + /** + * Gets all the parameters of the request. + * + * @see #getParameter(String) + * + * @see javax.servlet.ServletRequest#getParameterMap() + * @see javax.portlet.PortletRequest#getParameter(String) + * + * @return A mapping of parameter names to arrays of parameter values + */ + public Map<String, String[]> getParameterMap(); + + /** + * Returns the length of the request content that can be read from the input + * stream returned by {@link #getInputStream()}. + * + * @see javax.servlet.ServletRequest#getContentLength() + * @see javax.portlet.ClientDataRequest#getContentLength() + * + * @return content length in bytes + */ + public int getContentLength(); + + /** + * Returns an input stream from which the request content can be read. The + * request content length can be obtained with {@link #getContentLength()} + * without reading the full stream contents. + * + * @see javax.servlet.ServletRequest#getInputStream() + * @see javax.portlet.ClientDataRequest#getPortletInputStream() + * + * @return the input stream from which the contents of the request can be + * read + * @throws IOException + * if the input stream can not be opened + */ + public InputStream getInputStream() throws IOException; + + /** + * Gets a request attribute. + * + * @param name + * the name of the attribute + * @return the value of the attribute, or <code>null</code> if there is no + * attribute with the given name + * + * @see javax.servlet.ServletRequest#getAttribute(String) + * @see javax.portlet.PortletRequest#getAttribute(String) + */ + public Object getAttribute(String name); + + /** + * Defines a request attribute. + * + * @param name + * the name of the attribute + * @param value + * the attribute value + * + * @see javax.servlet.ServletRequest#setAttribute(String, Object) + * @see javax.portlet.PortletRequest#setAttribute(String, Object) + */ + public void setAttribute(String name, Object value); + + /** + * Gets the path of the requested resource relative to the application. The + * path be <code>null</code> if no path information is available. Does + * always start with / if the path isn't <code>null</code>. + * + * @return a string with the path relative to the application. + * + * @see javax.servlet.http.HttpServletRequest#getPathInfo() + */ + public String getRequestPathInfo(); + + /** + * Returns the maximum time interval, in seconds, that the session + * associated with this request will be kept open between client accesses. + * + * @return an integer specifying the number of seconds the session + * associated with this request remains open between client requests + * + * @see javax.servlet.http.HttpSession#getMaxInactiveInterval() + * @see javax.portlet.PortletSession#getMaxInactiveInterval() + */ + public int getSessionMaxInactiveInterval(); + + /** + * Gets an attribute from the session associated with this request. + * + * @param name + * the name of the attribute + * @return the attribute value, or <code>null</code> if the attribute is not + * defined in the session + * + * @see javax.servlet.http.HttpSession#getAttribute(String) + * @see javax.portlet.PortletSession#getAttribute(String) + */ + public Object getSessionAttribute(String name); + + /** + * Saves an attribute value in the session associated with this request. + * + * @param name + * the name of the attribute + * @param attribute + * the attribute value + * + * @see javax.servlet.http.HttpSession#setAttribute(String, Object) + * @see javax.portlet.PortletSession#setAttribute(String, Object) + */ + public void setSessionAttribute(String name, Object attribute); + + /** + * Returns the MIME type of the body of the request, or null if the type is + * not known. + * + * @return a string containing the name of the MIME type of the request, or + * null if the type is not known + * + * @see javax.servlet.ServletRequest#getContentType() + * @see javax.portlet.ResourceRequest#getContentType() + * + */ + public String getContentType(); + + /** + * Gets detailed information about the browser from which the request + * originated. This consists of information that is not available from + * normal HTTP requests, but requires additional information to be extracted + * for instance using javascript in the browser. + * + * This information is only guaranteed to be available in some special + * cases, for instance when {@link Application#getRoot} is called again + * after throwing {@link RootRequiresMoreInformationException} or in + * {@link Root#init(WrappedRequest)} for a Root class not annotated with + * {@link EagerInit} + * + * @return the browser details, or <code>null</code> if details are not + * available + * + * @see BrowserDetails + */ + public BrowserDetails getBrowserDetails(); + + /** + * Gets locale information from the query, e.g. using the Accept-Language + * header. + * + * @return the preferred Locale + * + * @see ServletRequest#getLocale() + * @see PortletRequest#getLocale() + */ + public Locale getLocale(); + + /** + * Returns the IP address from which the request came. This might also be + * the address of a proxy between the server and the original requester. + * + * @return a string containing the IP address, or <code>null</code> if the + * address is not available + * + * @see ServletRequest#getRemoteAddr() + */ + public String getRemoteAddr(); + + /** + * Checks whether the request was made using a secure channel, e.g. using + * https. + * + * @return a boolean indicating if the request is secure + * + * @see ServletRequest#isSecure() + * @see PortletRequest#isSecure() + */ + public boolean isSecure(); + + /** + * Gets the value of a request header, e.g. a http header for a + * {@link HttpServletRequest}. + * + * @param headerName + * the name of the header + * @return the header value, or <code>null</code> if the header is not + * present in the request + * + * @see HttpServletRequest#getHeader(String) + */ + public String getHeader(String headerName); + + /** + * Gets the deployment configuration for the context of this request. + * + * @return the deployment configuration + * + * @see DeploymentConfiguration + */ + public DeploymentConfiguration getDeploymentConfiguration(); + +} diff --git a/server/src/com/vaadin/terminal/WrappedResponse.java b/server/src/com/vaadin/terminal/WrappedResponse.java new file mode 100644 index 0000000000..995133a269 --- /dev/null +++ b/server/src/com/vaadin/terminal/WrappedResponse.java @@ -0,0 +1,147 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Serializable; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletResponse; +import javax.portlet.ResourceResponse; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * A generic response from the server, wrapping a more specific response type, + * e.g. HttpServletResponse or PortletResponse. + * + * @since 7.0 + */ +public interface WrappedResponse extends Serializable { + + /** + * Sets the (http) status code for the response. If you want to include an + * error message along the status code, use {@link #sendError(int, String)} + * instead. + * + * @param statusCode + * the status code to set + * @see HttpServletResponse#setStatus(int) + * + * @see ResourceResponse#HTTP_STATUS_CODE + */ + public void setStatus(int statusCode); + + /** + * Sets the content type of this response. If the content type including a + * charset is set before {@link #getWriter()} is invoked, the returned + * PrintWriter will automatically use the defined charset. + * + * @param contentType + * a string specifying the MIME type of the content + * + * @see ServletResponse#setContentType(String) + * @see MimeResponse#setContentType(String) + */ + public void setContentType(String contentType); + + /** + * Sets the value of a generic response header. If the header had already + * been set, the new value overwrites the previous one. + * + * @param name + * the name of the header + * @param value + * the header value. + * + * @see HttpServletResponse#setHeader(String, String) + * @see PortletResponse#setProperty(String, String) + */ + public void setHeader(String name, String value); + + /** + * Properly formats a timestamp as a date header. If the header had already + * been set, the new value overwrites the previous one. + * + * @param name + * the name of the header + * @param timestamp + * the number of milliseconds since epoch + * + * @see HttpServletResponse#setDateHeader(String, long) + */ + public void setDateHeader(String name, long timestamp); + + /** + * Returns a <code>OutputStream</code> for writing binary data in the + * response. + * <p> + * Either this method or getWriter() may be called to write the response, + * not both. + * + * @return a <code>OutputStream</code> for writing binary data + * @throws IOException + * if an input or output exception occurred + * + * @see #getWriter() + * @see ServletResponse#getOutputStream() + * @see MimeResponse#getPortletOutputStream() + */ + public OutputStream getOutputStream() throws IOException; + + /** + * Returns a <code>PrintWriter</code> object that can send character text to + * the client. The PrintWriter uses the character encoding defined using + * setContentType. + * <p> + * Either this method or getOutputStream() may be called to write the + * response, not both. + * + * @return a <code>PrintWriter</code> for writing character text + * @throws IOException + * if an input or output exception occurred + * + * @see #getOutputStream() + * @see ServletResponse#getWriter() + * @see MimeResponse#getWriter() + */ + public PrintWriter getWriter() throws IOException; + + /** + * Sets cache time in milliseconds, -1 means no cache at all. All required + * headers related to caching in the response are set based on the time. + * + * @param milliseconds + * Cache time in milliseconds + */ + public void setCacheTime(long milliseconds); + + /** + * Sends an error response to the client using the specified status code and + * clears the buffer. In some configurations, this can cause a predefined + * error page to be displayed. + * + * @param errorCode + * the HTTP status code + * @param message + * a message to accompany the error + * @throws IOException + * if an input or output exception occurs + * + * @see HttpServletResponse#sendError(int, String) + */ + public void sendError(int errorCode, String message) throws IOException; + + /** + * Gets the deployment configuration for the context of this response. + * + * @return the deployment configuration + * + * @see DeploymentConfiguration + */ + public DeploymentConfiguration getDeploymentConfiguration(); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java new file mode 100644 index 0000000000..40958e2868 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java @@ -0,0 +1,1079 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.security.GeneralSecurityException; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; +import javax.portlet.GenericPortlet; +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import com.liferay.portal.kernel.util.PortalClassInvoker; +import com.liferay.portal.kernel.util.PropsUtil; +import com.vaadin.Application; +import com.vaadin.Application.ApplicationStartEvent; +import com.vaadin.Application.SystemMessages; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.server.AbstractCommunicationManager.Callback; +import com.vaadin.ui.Root; + +/** + * Portlet 2.0 base class. This replaces the servlet in servlet/portlet 1.0 + * deployments and handles various portlet requests from the browser. + * + * TODO Document me! + * + * @author peholmst + */ +public abstract class AbstractApplicationPortlet extends GenericPortlet + implements Constants { + + public static final String RESOURCE_URL_ID = "APP"; + + public static class WrappedHttpAndPortletRequest extends + WrappedPortletRequest { + + public WrappedHttpAndPortletRequest(PortletRequest request, + HttpServletRequest originalRequest, + DeploymentConfiguration deploymentConfiguration) { + super(request, deploymentConfiguration); + this.originalRequest = originalRequest; + } + + private final HttpServletRequest originalRequest; + + @Override + public String getParameter(String name) { + String parameter = super.getParameter(name); + if (parameter == null) { + parameter = originalRequest.getParameter(name); + } + return parameter; + } + + @Override + public String getRemoteAddr() { + return originalRequest.getRemoteAddr(); + } + + @Override + public String getHeader(String name) { + String header = super.getHeader(name); + if (header == null) { + header = originalRequest.getHeader(name); + } + return header; + } + + @Override + public Map<String, String[]> getParameterMap() { + Map<String, String[]> parameterMap = super.getParameterMap(); + if (parameterMap == null) { + parameterMap = originalRequest.getParameterMap(); + } + return parameterMap; + } + } + + public static class WrappedGateinRequest extends + WrappedHttpAndPortletRequest { + public WrappedGateinRequest(PortletRequest request, + DeploymentConfiguration deploymentConfiguration) { + super(request, getOriginalRequest(request), deploymentConfiguration); + } + + private static final HttpServletRequest getOriginalRequest( + PortletRequest request) { + try { + Method getRealReq = request.getClass().getMethod( + "getRealRequest"); + HttpServletRequestWrapper origRequest = (HttpServletRequestWrapper) getRealReq + .invoke(request); + return origRequest; + } catch (Exception e) { + throw new IllegalStateException("GateIn request not detected", + e); + } + } + } + + public static class WrappedLiferayRequest extends + WrappedHttpAndPortletRequest { + + public WrappedLiferayRequest(PortletRequest request, + DeploymentConfiguration deploymentConfiguration) { + super(request, getOriginalRequest(request), deploymentConfiguration); + } + + @Override + public String getPortalProperty(String name) { + return PropsUtil.get(name); + } + + private static HttpServletRequest getOriginalRequest( + PortletRequest request) { + try { + // httpRequest = PortalUtil.getHttpServletRequest(request); + HttpServletRequest httpRequest = (HttpServletRequest) PortalClassInvoker + .invoke("com.liferay.portal.util.PortalUtil", + "getHttpServletRequest", request); + + // httpRequest = + // PortalUtil.getOriginalServletRequest(httpRequest); + httpRequest = (HttpServletRequest) PortalClassInvoker.invoke( + "com.liferay.portal.util.PortalUtil", + "getOriginalServletRequest", httpRequest); + return httpRequest; + } catch (Exception e) { + throw new IllegalStateException("Liferay request not detected", + e); + } + } + + } + + public static class AbstractApplicationPortletWrapper implements Callback { + + private final AbstractApplicationPortlet portlet; + + public AbstractApplicationPortletWrapper( + AbstractApplicationPortlet portlet) { + this.portlet = portlet; + } + + @Override + public void criticalNotification(WrappedRequest request, + WrappedResponse response, String cap, String msg, + String details, String outOfSyncURL) throws IOException { + portlet.criticalNotification(WrappedPortletRequest.cast(request), + (WrappedPortletResponse) response, cap, msg, details, + outOfSyncURL); + } + } + + /** + * This portlet parameter is used to add styles to the main element. E.g + * "height:500px" generates a style="height:500px" to the main element. + */ + public static final String PORTLET_PARAMETER_STYLE = "style"; + + /** + * This portal parameter is used to define the name of the Vaadin theme that + * is used for all Vaadin applications in the portal. + */ + public static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme"; + + public static final String WRITE_AJAX_PAGE_SCRIPT_WIDGETSET_SHOULD_WRITE = "writeAjaxPageScriptWidgetsetShouldWrite"; + + // TODO some parts could be shared with AbstractApplicationServlet + + // TODO Can we close the application when the portlet is removed? Do we know + // when the portlet is removed? + + private boolean productionMode = false; + + private DeploymentConfiguration deploymentConfiguration = new AbstractDeploymentConfiguration( + getClass()) { + @Override + public String getConfiguredWidgetset(WrappedRequest request) { + + String widgetset = getApplicationOrSystemProperty( + PARAMETER_WIDGETSET, null); + + if (widgetset == null) { + // If no widgetset defined for the application, check the + // portal + // property + widgetset = WrappedPortletRequest.cast(request) + .getPortalProperty(PORTAL_PARAMETER_VAADIN_WIDGETSET); + } + + if (widgetset == null) { + // If no widgetset defined for the portal, use the default + widgetset = DEFAULT_WIDGETSET; + } + + return widgetset; + } + + @Override + public String getConfiguredTheme(WrappedRequest request) { + + // is the default theme defined by the portal? + String themeName = WrappedPortletRequest.cast(request) + .getPortalProperty(Constants.PORTAL_PARAMETER_VAADIN_THEME); + + if (themeName == null) { + // no, using the default theme defined by Vaadin + themeName = DEFAULT_THEME_NAME; + } + + return themeName; + } + + @Override + public boolean isStandalone(WrappedRequest request) { + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.DeploymentConfiguration#getStaticFileLocation + * (com.vaadin.terminal.WrappedRequest) + * + * Return the URL from where static files, e.g. the widgetset and the + * theme, are served. In a standard configuration the VAADIN folder + * inside the returned folder is what is used for widgetsets and themes. + * + * @return The location of static resources (inside which there should + * be a VAADIN directory). Does not end with a slash (/). + */ + + @Override + public String getStaticFileLocation(WrappedRequest request) { + String staticFileLocation = WrappedPortletRequest.cast(request) + .getPortalProperty( + Constants.PORTAL_PARAMETER_VAADIN_RESOURCE_PATH); + if (staticFileLocation != null) { + // remove trailing slash if any + while (staticFileLocation.endsWith(".")) { + staticFileLocation = staticFileLocation.substring(0, + staticFileLocation.length() - 1); + } + return staticFileLocation; + } else { + // default for Liferay + return "/html"; + } + } + + @Override + public String getMimeType(String resourceName) { + return getPortletContext().getMimeType(resourceName); + } + }; + + private final AddonContext addonContext = new AddonContext( + getDeploymentConfiguration()); + + @Override + public void init(PortletConfig config) throws PortletException { + super.init(config); + Properties applicationProperties = getDeploymentConfiguration() + .getInitParameters(); + + // Read default parameters from the context + final PortletContext context = config.getPortletContext(); + for (final Enumeration<String> e = context.getInitParameterNames(); e + .hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + context.getInitParameter(name)); + } + + // Override with application settings from portlet.xml + for (final Enumeration<String> e = config.getInitParameterNames(); e + .hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + config.getInitParameter(name)); + } + + checkProductionMode(); + checkCrossSiteProtection(); + + addonContext.init(); + } + + @Override + public void destroy() { + super.destroy(); + + addonContext.destroy(); + } + + private void checkCrossSiteProtection() { + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION, "false").equals( + "true")) { + /* + * Print an information/warning message about running with xsrf + * protection disabled + */ + getLogger().warning(WARNING_XSRF_PROTECTION_DISABLED); + } + } + + private void checkProductionMode() { + // TODO Identical code in AbstractApplicationServlet -> refactor + // Check if the application is in production mode. + // We are in production mode if productionMode=true + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) { + productionMode = true; + } + + if (!productionMode) { + /* Print an information/warning message about running in debug mode */ + // TODO Maybe we need a different message for portlets? + getLogger().warning(NOT_PRODUCTION_MODE_INFO); + } + } + + protected enum RequestType { + FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE; + } + + protected RequestType getRequestType(WrappedPortletRequest wrappedRequest) { + PortletRequest request = wrappedRequest.getPortletRequest(); + if (request instanceof RenderRequest) { + return RequestType.RENDER; + } else if (request instanceof ResourceRequest) { + ResourceRequest resourceRequest = (ResourceRequest) request; + if (ServletPortletHelper.isUIDLRequest(wrappedRequest)) { + return RequestType.UIDL; + } else if (isBrowserDetailsRequest(resourceRequest)) { + return RequestType.BROWSER_DETAILS; + } else if (ServletPortletHelper.isFileUploadRequest(wrappedRequest)) { + return RequestType.FILE_UPLOAD; + } else if (ServletPortletHelper + .isConnectorResourceRequest(wrappedRequest)) { + return RequestType.CONNECTOR_RESOURCE; + } else if (ServletPortletHelper + .isApplicationResourceRequest(wrappedRequest)) { + return RequestType.APPLICATION_RESOURCE; + } else if (isDummyRequest(resourceRequest)) { + return RequestType.DUMMY; + } else { + return RequestType.STATIC_FILE; + } + } else if (request instanceof ActionRequest) { + return RequestType.ACTION; + } else if (request instanceof EventRequest) { + return RequestType.EVENT; + } + return RequestType.UNKNOWN; + } + + private boolean isBrowserDetailsRequest(ResourceRequest request) { + return request.getResourceID() != null + && request.getResourceID().equals("browserDetails"); + } + + private boolean isDummyRequest(ResourceRequest request) { + return request.getResourceID() != null + && request.getResourceID().equals("DUMMY"); + } + + /** + * Returns true if the servlet is running in production mode. Production + * mode disables all debug facilities. + * + * @return true if in production mode, false if in debug mode + */ + public boolean isProductionMode() { + return productionMode; + } + + protected void handleRequest(PortletRequest request, + PortletResponse response) throws PortletException, IOException { + RequestTimer requestTimer = new RequestTimer(); + requestTimer.start(); + + AbstractApplicationPortletWrapper portletWrapper = new AbstractApplicationPortletWrapper( + this); + + WrappedPortletRequest wrappedRequest = createWrappedRequest(request); + + WrappedPortletResponse wrappedResponse = new WrappedPortletResponse( + response, getDeploymentConfiguration()); + + RequestType requestType = getRequestType(wrappedRequest); + + if (requestType == RequestType.UNKNOWN) { + handleUnknownRequest(request, response); + } else if (requestType == RequestType.DUMMY) { + /* + * This dummy page is used by action responses to redirect to, in + * order to prevent the boot strap code from being rendered into + * strange places such as iframes. + */ + ((ResourceResponse) response).setContentType("text/html"); + final OutputStream out = ((ResourceResponse) response) + .getPortletOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>dummy page</body></html>"); + outWriter.close(); + } else if (requestType == RequestType.STATIC_FILE) { + serveStaticResources((ResourceRequest) request, + (ResourceResponse) response); + } else { + Application application = null; + boolean transactionStarted = false; + boolean requestStarted = false; + + try { + // TODO What about PARAM_UNLOADBURST & redirectToApplication?? + + /* Find out which application this request is related to */ + application = findApplicationInstance(wrappedRequest, + requestType); + if (application == null) { + return; + } + Application.setCurrent(application); + + /* + * Get or create an application context and an application + * manager for the session + */ + PortletApplicationContext2 applicationContext = getApplicationContext(request + .getPortletSession()); + applicationContext.setResponse(response); + applicationContext.setPortletConfig(getPortletConfig()); + + PortletCommunicationManager applicationManager = applicationContext + .getApplicationManager(application); + + if (requestType == RequestType.CONNECTOR_RESOURCE) { + applicationManager.serveConnectorResource(wrappedRequest, + wrappedResponse); + return; + } + + /* Update browser information from request */ + applicationContext.getBrowser().updateRequestDetails( + wrappedRequest); + + /* + * Call application requestStart before Application.init() is + * called (bypasses the limitation in TransactionListener) + */ + if (application instanceof PortletRequestListener) { + ((PortletRequestListener) application).onRequestStart( + request, response); + requestStarted = true; + } + + /* Start the newly created application */ + startApplication(request, application, applicationContext); + + /* + * Transaction starts. Call transaction listeners. Transaction + * end is called in the finally block below. + */ + applicationContext.startTransaction(application, request); + transactionStarted = true; + + /* Notify listeners */ + + // Finds the window within the application + Root root = null; + synchronized (application) { + if (application.isRunning()) { + switch (requestType) { + case RENDER: + case ACTION: + // Both action requests and render requests are ok + // without a Root as they render the initial HTML + // and then do a second request + try { + root = application + .getRootForRequest(wrappedRequest); + } catch (RootRequiresMoreInformationException e) { + // Ignore problem and continue without root + } + break; + case BROWSER_DETAILS: + // Should not try to find a root here as the + // combined request details might change the root + break; + case FILE_UPLOAD: + // no window + break; + case APPLICATION_RESOURCE: + // use main window - should not need any window + // root = application.getRoot(); + break; + default: + root = application + .getRootForRequest(wrappedRequest); + } + // if window not found, not a problem - use null + } + } + + // TODO Should this happen before or after the transaction + // starts? + if (request instanceof RenderRequest) { + applicationContext.firePortletRenderRequest(application, + root, (RenderRequest) request, + (RenderResponse) response); + } else if (request instanceof ActionRequest) { + applicationContext.firePortletActionRequest(application, + root, (ActionRequest) request, + (ActionResponse) response); + } else if (request instanceof EventRequest) { + applicationContext.firePortletEventRequest(application, + root, (EventRequest) request, + (EventResponse) response); + } else if (request instanceof ResourceRequest) { + applicationContext.firePortletResourceRequest(application, + root, (ResourceRequest) request, + (ResourceResponse) response); + } + + /* Handle the request */ + if (requestType == RequestType.FILE_UPLOAD) { + // Root is resolved in handleFileUpload by + // PortletCommunicationManager + applicationManager.handleFileUpload(application, + wrappedRequest, wrappedResponse); + return; + } else if (requestType == RequestType.BROWSER_DETAILS) { + applicationManager.handleBrowserDetailsRequest( + wrappedRequest, wrappedResponse, application); + return; + } else if (requestType == RequestType.UIDL) { + // Handles AJAX UIDL requests + applicationManager.handleUidlRequest(wrappedRequest, + wrappedResponse, portletWrapper, root); + return; + } else { + /* + * Removes the application if it has stopped + */ + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + handleOtherRequest(wrappedRequest, wrappedResponse, + requestType, application, applicationContext, + applicationManager); + } + } catch (final SessionExpiredException e) { + // TODO Figure out a better way to deal with + // SessionExpiredExceptions + getLogger().finest("A user session has expired"); + } catch (final GeneralSecurityException e) { + // TODO Figure out a better way to deal with + // GeneralSecurityExceptions + getLogger() + .fine("General security exception, the security key was probably incorrect."); + } catch (final Throwable e) { + handleServiceException(wrappedRequest, wrappedResponse, + application, e); + } finally { + // Notifies transaction end + try { + if (transactionStarted) { + ((PortletApplicationContext2) application.getContext()) + .endTransaction(application, request); + } + } finally { + try { + if (requestStarted) { + ((PortletRequestListener) application) + .onRequestEnd(request, response); + + } + } finally { + Root.setCurrent(null); + Application.setCurrent(null); + + PortletSession session = request + .getPortletSession(false); + if (session != null) { + requestTimer.stop(getApplicationContext(session)); + } + } + } + } + } + } + + /** + * Wraps the request in a (possibly portal specific) wrapped portlet + * request. + * + * @param request + * The original PortletRequest + * @return A wrapped version of the PorletRequest + */ + protected WrappedPortletRequest createWrappedRequest(PortletRequest request) { + String portalInfo = request.getPortalContext().getPortalInfo() + .toLowerCase(); + if (portalInfo.contains("liferay")) { + return new WrappedLiferayRequest(request, + getDeploymentConfiguration()); + } else if (portalInfo.contains("gatein")) { + return new WrappedGateinRequest(request, + getDeploymentConfiguration()); + } else { + return new WrappedPortletRequest(request, + getDeploymentConfiguration()); + } + + } + + protected DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + private void handleUnknownRequest(PortletRequest request, + PortletResponse response) { + getLogger().warning("Unknown request type"); + } + + /** + * Handle a portlet request that is not for static files, UIDL or upload. + * Also render requests are handled here. + * + * This method is called after starting the application and calling portlet + * and transaction listeners. + * + * @param request + * @param response + * @param requestType + * @param application + * @param applicationContext + * @param applicationManager + * @throws PortletException + * @throws IOException + * @throws MalformedURLException + */ + private void handleOtherRequest(WrappedPortletRequest request, + WrappedResponse response, RequestType requestType, + Application application, + PortletApplicationContext2 applicationContext, + PortletCommunicationManager applicationManager) + throws PortletException, IOException, MalformedURLException { + if (requestType == RequestType.APPLICATION_RESOURCE + || requestType == RequestType.RENDER) { + if (!applicationManager.handleApplicationRequest(request, response)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + "Not found"); + } + } else if (requestType == RequestType.EVENT) { + // nothing to do, listeners do all the work + } else if (requestType == RequestType.ACTION) { + // nothing to do, listeners do all the work + } else { + throw new IllegalStateException( + "handleRequest() without anything to do - should never happen!"); + } + } + + @Override + public void processEvent(EventRequest request, EventResponse response) + throws PortletException, IOException { + handleRequest(request, response); + } + + private void serveStaticResources(ResourceRequest request, + ResourceResponse response) throws IOException, PortletException { + final String resourceID = request.getResourceID(); + final PortletContext pc = getPortletContext(); + + InputStream is = pc.getResourceAsStream(resourceID); + if (is != null) { + final String mimetype = pc.getMimeType(resourceID); + if (mimetype != null) { + response.setContentType(mimetype); + } + final OutputStream os = response.getPortletOutputStream(); + final byte buffer[] = new byte[DEFAULT_BUFFER_SIZE]; + int bytes; + while ((bytes = is.read(buffer)) >= 0) { + os.write(buffer, 0, bytes); + } + } else { + getLogger().info( + "Requested resource [" + resourceID + + "] could not be found"); + response.setProperty(ResourceResponse.HTTP_STATUS_CODE, + Integer.toString(HttpServletResponse.SC_NOT_FOUND)); + } + } + + @Override + public void processAction(ActionRequest request, ActionResponse response) + throws PortletException, IOException { + handleRequest(request, response); + } + + @Override + protected void doDispatch(RenderRequest request, RenderResponse response) + throws PortletException, IOException { + try { + // try to let super handle - it'll call methods annotated for + // handling, the default doXYZ(), or throw if a handler for the mode + // is not found + super.doDispatch(request, response); + + } catch (PortletException e) { + if (e.getCause() == null) { + // No cause interpreted as 'unknown mode' - pass that trough + // so that the application can handle + handleRequest(request, response); + + } else { + // Something else failed, pass on + throw e; + } + } + } + + @Override + public void serveResource(ResourceRequest request, ResourceResponse response) + throws PortletException, IOException { + handleRequest(request, response); + } + + boolean requestCanCreateApplication(PortletRequest request, + RequestType requestType) { + if (requestType == RequestType.UIDL && isRepaintAll(request)) { + return true; + } else if (requestType == RequestType.RENDER) { + // In most cases the first request is a render request that renders + // the HTML fragment. This should create an application instance. + return true; + } else if (requestType == RequestType.EVENT) { + // A portlet can also be sent an event even though it has not been + // rendered, e.g. portlet on one page sends an event to a portlet on + // another page and then moves the user to that page. + return true; + } + return false; + } + + private boolean isRepaintAll(PortletRequest request) { + return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null) + && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1")); + } + + private void startApplication(PortletRequest request, + Application application, PortletApplicationContext2 context) + throws PortletException, MalformedURLException { + if (!application.isRunning()) { + Locale locale = request.getLocale(); + application.setLocale(locale); + // No application URL when running inside a portlet + application.start(new ApplicationStartEvent(null, + getDeploymentConfiguration().getInitParameters(), context, + isProductionMode())); + addonContext.applicationStarted(application); + } + } + + private void endApplication(PortletRequest request, + PortletResponse response, Application application) + throws IOException { + final PortletSession session = request.getPortletSession(); + if (session != null) { + getApplicationContext(session).removeApplication(application); + } + // Do not send any redirects when running inside a portlet. + } + + private Application findApplicationInstance( + WrappedPortletRequest wrappedRequest, RequestType requestType) + throws PortletException, SessionExpiredException, + MalformedURLException { + PortletRequest request = wrappedRequest.getPortletRequest(); + + boolean requestCanCreateApplication = requestCanCreateApplication( + request, requestType); + + /* Find an existing application for this request. */ + Application application = getExistingApplication(request, + requestCanCreateApplication); + + if (application != null) { + /* + * There is an existing application. We can use this as long as the + * user not specifically requested to close or restart it. + */ + + final boolean restartApplication = (wrappedRequest + .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null); + final boolean closeApplication = (wrappedRequest + .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null); + + if (restartApplication) { + closeApplication(application, request.getPortletSession(false)); + return createApplication(request); + } else if (closeApplication) { + closeApplication(application, request.getPortletSession(false)); + return null; + } else { + return application; + } + } + + // No existing application was found + + if (requestCanCreateApplication) { + return createApplication(request); + } else { + throw new SessionExpiredException(); + } + } + + private void closeApplication(Application application, + PortletSession session) { + if (application == null) { + return; + } + + application.close(); + if (session != null) { + PortletApplicationContext2 context = getApplicationContext(session); + context.removeApplication(application); + } + } + + private Application createApplication(PortletRequest request) + throws PortletException, MalformedURLException { + Application newApplication = getNewApplication(request); + final PortletApplicationContext2 context = getApplicationContext(request + .getPortletSession()); + context.addApplication(newApplication, request.getWindowID()); + return newApplication; + } + + private Application getExistingApplication(PortletRequest request, + boolean allowSessionCreation) throws MalformedURLException, + SessionExpiredException { + + final PortletSession session = request + .getPortletSession(allowSessionCreation); + + if (session == null) { + throw new SessionExpiredException(); + } + + PortletApplicationContext2 context = getApplicationContext(session); + Application application = context.getApplicationForWindowId(request + .getWindowID()); + if (application == null) { + return null; + } + if (application.isRunning()) { + return application; + } + // application found but not running + context.removeApplication(application); + + return null; + } + + protected abstract Class<? extends Application> getApplicationClass() + throws ClassNotFoundException; + + protected Application getNewApplication(PortletRequest request) + throws PortletException { + try { + final Application application = getApplicationClass().newInstance(); + application.setRootPreserved(true); + return application; + } catch (final IllegalAccessException e) { + throw new PortletException("getNewApplication failed", e); + } catch (final InstantiationException e) { + throw new PortletException("getNewApplication failed", e); + } catch (final ClassNotFoundException e) { + throw new PortletException("getNewApplication failed", e); + } + } + + /** + * Get system messages from the current application class + * + * @return + */ + protected SystemMessages getSystemMessages() { + try { + Class<? extends Application> appCls = getApplicationClass(); + Method m = appCls.getMethod("getSystemMessages", (Class[]) null); + return (Application.SystemMessages) m.invoke(null, (Object[]) null); + } catch (ClassNotFoundException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (SecurityException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (NoSuchMethodException e) { + // This is completely ok and should be silently ignored + } catch (IllegalArgumentException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (IllegalAccessException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (InvocationTargetException e) { + // This should never happen + throw new SystemMessageException(e); + } + return Application.getSystemMessages(); + } + + private void handleServiceException(WrappedPortletRequest request, + WrappedPortletResponse response, Application application, + Throwable e) throws IOException, PortletException { + // TODO Check that this error handler is working when running inside a + // portlet + + // if this was an UIDL request, response UIDL back to client + if (getRequestType(request) == RequestType.UIDL) { + Application.SystemMessages ci = getSystemMessages(); + criticalNotification(request, response, + ci.getInternalErrorCaption(), ci.getInternalErrorMessage(), + null, ci.getInternalErrorURL()); + if (application != null) { + application.getErrorHandler() + .terminalError(new RequestError(e)); + } else { + throw new PortletException(e); + } + } else { + // Re-throw other exceptions + throw new PortletException(e); + } + + } + + @SuppressWarnings("serial") + public class RequestError implements Terminal.ErrorEvent, Serializable { + + private final Throwable throwable; + + public RequestError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Send notification to client's application. Used to notify client of + * critical errors and session expiration due to long inactivity. Server has + * no knowledge of what application client refers to. + * + * @param request + * the Portlet request instance. + * @param response + * the Portlet response to write to. + * @param caption + * for the notification + * @param message + * for the notification + * @param details + * a detail message to show in addition to the passed message. + * Currently shown directly but could be hidden behind a details + * drop down. + * @param url + * url to load after message, null for current page + * @throws IOException + * if the writing failed due to input/output error. + */ + void criticalNotification(WrappedPortletRequest request, + WrappedPortletResponse response, String caption, String message, + String details, String url) throws IOException { + + // clients JS app is still running, but server application either + // no longer exists or it might fail to perform reasonably. + // send a notification to client's application and link how + // to "restart" application. + + if (caption != null) { + caption = "\"" + caption + "\""; + } + if (details != null) { + if (message == null) { + message = details; + } else { + message += "<br/><br/>" + details; + } + } + if (message != null) { + message = "\"" + message + "\""; + } + if (url != null) { + url = "\"" + url + "\""; + } + + // Set the response type + response.setContentType("application/json; charset=UTF-8"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("for(;;);[{\"changes\":[], \"meta\" : {" + + "\"appError\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}}, \"resources\": {}, \"locales\":[]}]"); + outWriter.close(); + } + + /** + * + * Gets the application context for a PortletSession. If no context is + * currently stored in a session a new context is created and stored in the + * session. + * + * @param portletSession + * the portlet session. + * @return the application context for the session. + */ + protected PortletApplicationContext2 getApplicationContext( + PortletSession portletSession) { + return PortletApplicationContext2.getApplicationContext(portletSession); + } + + private static final Logger getLogger() { + return Logger.getLogger(AbstractApplicationPortlet.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java new file mode 100644 index 0000000000..603bc74a21 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -0,0 +1,1623 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.vaadin.Application; +import com.vaadin.Application.ApplicationStartEvent; +import com.vaadin.Application.SystemMessages; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.ThemeResource; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.AbstractCommunicationManager.Callback; +import com.vaadin.ui.Root; + +/** + * Abstract implementation of the ApplicationServlet which handles all + * communication between the client and the server. + * + * It is possible to extend this class to provide own functionality but in most + * cases this is unnecessary. + * + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.0 + */ + +@SuppressWarnings("serial") +public abstract class AbstractApplicationServlet extends HttpServlet implements + Constants { + + private static class AbstractApplicationServletWrapper implements Callback { + + private final AbstractApplicationServlet servlet; + + public AbstractApplicationServletWrapper( + AbstractApplicationServlet servlet) { + this.servlet = servlet; + } + + @Override + public void criticalNotification(WrappedRequest request, + WrappedResponse response, String cap, String msg, + String details, String outOfSyncURL) throws IOException { + servlet.criticalNotification( + WrappedHttpServletRequest.cast(request), + ((WrappedHttpServletResponse) response), cap, msg, details, + outOfSyncURL); + } + } + + // TODO Move some (all?) of the constants to a separate interface (shared + // with portlet) + + private boolean productionMode = false; + + private final String resourcePath = null; + + private int resourceCacheTime = 3600; + + private DeploymentConfiguration deploymentConfiguration = new AbstractDeploymentConfiguration( + getClass()) { + + @Override + public String getStaticFileLocation(WrappedRequest request) { + HttpServletRequest servletRequest = WrappedHttpServletRequest + .cast(request); + return AbstractApplicationServlet.this + .getStaticFilesLocation(servletRequest); + } + + @Override + public String getConfiguredWidgetset(WrappedRequest request) { + return getApplicationOrSystemProperty( + AbstractApplicationServlet.PARAMETER_WIDGETSET, + AbstractApplicationServlet.DEFAULT_WIDGETSET); + } + + @Override + public String getConfiguredTheme(WrappedRequest request) { + // Use the default + return AbstractApplicationServlet.getDefaultTheme(); + } + + @Override + public boolean isStandalone(WrappedRequest request) { + return true; + } + + @Override + public String getMimeType(String resourceName) { + return getServletContext().getMimeType(resourceName); + } + }; + + private final AddonContext addonContext = new AddonContext( + getDeploymentConfiguration()); + + /** + * Called by the servlet container to indicate to a servlet that the servlet + * is being placed into service. + * + * @param servletConfig + * the object containing the servlet's configuration and + * initialization parameters + * @throws javax.servlet.ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + @Override + public void init(javax.servlet.ServletConfig servletConfig) + throws javax.servlet.ServletException { + super.init(servletConfig); + Properties applicationProperties = getDeploymentConfiguration() + .getInitParameters(); + + // Read default parameters from server.xml + final ServletContext context = servletConfig.getServletContext(); + for (final Enumeration<String> e = context.getInitParameterNames(); e + .hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + context.getInitParameter(name)); + } + + // Override with application config from web.xml + for (final Enumeration<String> e = servletConfig + .getInitParameterNames(); e.hasMoreElements();) { + final String name = e.nextElement(); + applicationProperties.setProperty(name, + servletConfig.getInitParameter(name)); + } + + checkProductionMode(); + checkCrossSiteProtection(); + checkResourceCacheTime(); + + addonContext.init(); + } + + @Override + public void destroy() { + super.destroy(); + + addonContext.destroy(); + } + + private void checkCrossSiteProtection() { + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION, "false").equals( + "true")) { + /* + * Print an information/warning message about running with xsrf + * protection disabled + */ + getLogger().warning(WARNING_XSRF_PROTECTION_DISABLED); + } + } + + private void checkProductionMode() { + // Check if the application is in production mode. + // We are in production mode if productionMode=true + if (getDeploymentConfiguration().getApplicationOrSystemProperty( + SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) { + productionMode = true; + } + + if (!productionMode) { + /* Print an information/warning message about running in debug mode */ + getLogger().warning(NOT_PRODUCTION_MODE_INFO); + } + + } + + private void checkResourceCacheTime() { + // Check if the browser caching time has been set in web.xml + try { + String rct = getDeploymentConfiguration() + .getApplicationOrSystemProperty( + SERVLET_PARAMETER_RESOURCE_CACHE_TIME, "3600"); + resourceCacheTime = Integer.parseInt(rct); + } catch (NumberFormatException nfe) { + // Default is 1h + resourceCacheTime = 3600; + getLogger().warning(WARNING_RESOURCE_CACHING_TIME_NOT_NUMERIC); + } + } + + /** + * Returns true if the servlet is running in production mode. Production + * mode disables all debug facilities. + * + * @return true if in production mode, false if in debug mode + */ + public boolean isProductionMode() { + return productionMode; + } + + /** + * Returns the amount of milliseconds the browser should cache a file. + * Default is 1 hour (3600 ms). + * + * @return The amount of milliseconds files are cached in the browser + */ + public int getResourceCacheTime() { + return resourceCacheTime; + } + + /** + * Receives standard HTTP requests from the public service method and + * dispatches them. + * + * @param request + * the object that contains the request the client made of the + * servlet. + * @param response + * the object that contains the response the servlet returns to + * the client. + * @throws ServletException + * if an input or output error occurs while the servlet is + * handling the TRACE request. + * @throws IOException + * if the request for the TRACE cannot be handled. + */ + + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + service(createWrappedRequest(request), createWrappedResponse(response)); + } + + private void service(WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws ServletException, + IOException { + RequestTimer requestTimer = new RequestTimer(); + requestTimer.start(); + + AbstractApplicationServletWrapper servletWrapper = new AbstractApplicationServletWrapper( + this); + + RequestType requestType = getRequestType(request); + if (!ensureCookiesEnabled(requestType, request, response)) { + return; + } + + if (requestType == RequestType.STATIC_FILE) { + serveStaticResources(request, response); + return; + } + + Application application = null; + boolean transactionStarted = false; + boolean requestStarted = false; + + try { + // If a duplicate "close application" URL is received for an + // application that is not open, redirect to the application's main + // page. + // This is needed as e.g. Spring Security remembers the last + // URL from the application, which is the logout URL, and repeats + // it. + // We can tell apart a real onunload request from a repeated one + // based on the real one having content (at least the UIDL security + // key). + if (requestType == RequestType.UIDL + && request.getParameterMap().containsKey( + ApplicationConnection.PARAM_UNLOADBURST) + && request.getContentLength() < 1 + && getExistingApplication(request, false) == null) { + redirectToApplication(request, response); + return; + } + + // Find out which application this request is related to + application = findApplicationInstance(request, requestType); + if (application == null) { + return; + } + Application.setCurrent(application); + + /* + * Get or create a WebApplicationContext and an ApplicationManager + * for the session + */ + WebApplicationContext webApplicationContext = getApplicationContext(request + .getSession()); + CommunicationManager applicationManager = webApplicationContext + .getApplicationManager(application, this); + + if (requestType == RequestType.CONNECTOR_RESOURCE) { + applicationManager.serveConnectorResource(request, response); + return; + } + + /* Update browser information from the request */ + webApplicationContext.getBrowser().updateRequestDetails(request); + + /* + * Call application requestStart before Application.init() is called + * (bypasses the limitation in TransactionListener) + */ + if (application instanceof HttpServletRequestListener) { + ((HttpServletRequestListener) application).onRequestStart( + request, response); + requestStarted = true; + } + + // Start the application if it's newly created + startApplication(request, application, webApplicationContext); + + /* + * Transaction starts. Call transaction listeners. Transaction end + * is called in the finally block below. + */ + webApplicationContext.startTransaction(application, request); + transactionStarted = true; + + /* Handle the request */ + if (requestType == RequestType.FILE_UPLOAD) { + // Root is resolved in communication manager + applicationManager.handleFileUpload(application, request, + response); + return; + } else if (requestType == RequestType.UIDL) { + Root root = application.getRootForRequest(request); + if (root == null) { + throw new ServletException(ERROR_NO_ROOT_FOUND); + } + // Handles AJAX UIDL requests + applicationManager.handleUidlRequest(request, response, + servletWrapper, root); + return; + } else if (requestType == RequestType.BROWSER_DETAILS) { + // Browser details - not related to a specific root + applicationManager.handleBrowserDetailsRequest(request, + response, application); + return; + } + + // Removes application if it has stopped (maybe by thread or + // transactionlistener) + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + if (applicationManager.handleApplicationRequest(request, response)) { + return; + } + // TODO Should return 404 error here and not do anything more + + } catch (final SessionExpiredException e) { + // Session has expired, notify user + handleServiceSessionExpired(request, response); + } catch (final GeneralSecurityException e) { + handleServiceSecurityException(request, response); + } catch (final Throwable e) { + handleServiceException(request, response, application, e); + } finally { + // Notifies transaction end + try { + if (transactionStarted) { + ((WebApplicationContext) application.getContext()) + .endTransaction(application, request); + + } + + } finally { + try { + if (requestStarted) { + ((HttpServletRequestListener) application) + .onRequestEnd(request, response); + } + } finally { + Root.setCurrent(null); + Application.setCurrent(null); + + HttpSession session = request.getSession(false); + if (session != null) { + requestTimer.stop(getApplicationContext(session)); + } + } + } + + } + } + + private WrappedHttpServletResponse createWrappedResponse( + HttpServletResponse response) { + WrappedHttpServletResponse wrappedResponse = new WrappedHttpServletResponse( + response, getDeploymentConfiguration()); + return wrappedResponse; + } + + /** + * Create a wrapped request for a http servlet request. This method can be + * overridden if the wrapped request should have special properties. + * + * @param request + * the original http servlet request + * @return a wrapped request for the original request + */ + protected WrappedHttpServletRequest createWrappedRequest( + HttpServletRequest request) { + return new WrappedHttpServletRequest(request, + getDeploymentConfiguration()); + } + + /** + * Gets a the deployment configuration for this servlet. + * + * @return the deployment configuration + */ + protected DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + /** + * Check that cookie support is enabled in the browser. Only checks UIDL + * requests. + * + * @param requestType + * Type of the request as returned by + * {@link #getRequestType(HttpServletRequest)} + * @param request + * The request from the browser + * @param response + * The response to which an error can be written + * @return false if cookies are disabled, true otherwise + * @throws IOException + */ + private boolean ensureCookiesEnabled(RequestType requestType, + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + if (requestType == RequestType.UIDL && !isRepaintAll(request)) { + // In all other but the first UIDL request a cookie should be + // returned by the browser. + // This can be removed if cookieless mode (#3228) is supported + if (request.getRequestedSessionId() == null) { + // User has cookies disabled + criticalNotification(request, response, getSystemMessages() + .getCookiesDisabledCaption(), getSystemMessages() + .getCookiesDisabledMessage(), null, getSystemMessages() + .getCookiesDisabledURL()); + return false; + } + } + return true; + } + + /** + * Send a notification to client's application. Used to notify client of + * critical errors, session expiration and more. Server has no knowledge of + * what application client refers to. + * + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @param caption + * the notification caption + * @param message + * to notification body + * @param details + * a detail message to show in addition to the message. Currently + * shown directly below the message but could be hidden behind a + * details drop down in the future. Mainly used to give + * additional information not necessarily useful to the end user. + * @param url + * url to load when the message is dismissed. Null will reload + * the current page. + * @throws IOException + * if the writing failed due to input/output error. + */ + protected void criticalNotification(WrappedHttpServletRequest request, + HttpServletResponse response, String caption, String message, + String details, String url) throws IOException { + + if (ServletPortletHelper.isUIDLRequest(request)) { + + if (caption != null) { + caption = "\"" + JsonPaintTarget.escapeJSON(caption) + "\""; + } + if (details != null) { + if (message == null) { + message = details; + } else { + message += "<br/><br/>" + details; + } + } + + if (message != null) { + message = "\"" + JsonPaintTarget.escapeJSON(message) + "\""; + } + if (url != null) { + url = "\"" + JsonPaintTarget.escapeJSON(url) + "\""; + } + + String output = "for(;;);[{\"changes\":[], \"meta\" : {" + + "\"appError\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}}, \"resources\": {}, \"locales\":[]}]"; + writeResponse(response, "application/json; charset=UTF-8", output); + } else { + // Create an HTML reponse with the error + String output = ""; + + if (url != null) { + output += "<a href=\"" + url + "\">"; + } + if (caption != null) { + output += "<b>" + caption + "</b><br/>"; + } + if (message != null) { + output += message; + output += "<br/><br/>"; + } + + if (details != null) { + output += details; + output += "<br/><br/>"; + } + if (url != null) { + output += "</a>"; + } + writeResponse(response, "text/html; charset=UTF-8", output); + + } + + } + + /** + * Writes the response in {@code output} using the contentType given in + * {@code contentType} to the provided {@link HttpServletResponse} + * + * @param response + * @param contentType + * @param output + * Output to write (UTF-8 encoded) + * @throws IOException + */ + private void writeResponse(HttpServletResponse response, + String contentType, String output) throws IOException { + response.setContentType(contentType); + final ServletOutputStream out = response.getOutputStream(); + // Set the response type + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print(output); + outWriter.flush(); + outWriter.close(); + out.flush(); + + } + + /** + * Returns the application instance to be used for the request. If an + * existing instance is not found a new one is created or null is returned + * to indicate that the application is not available. + * + * @param request + * @param requestType + * @return + * @throws MalformedURLException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws ServletException + * @throws SessionExpiredException + */ + private Application findApplicationInstance(HttpServletRequest request, + RequestType requestType) throws MalformedURLException, + ServletException, SessionExpiredException { + + boolean requestCanCreateApplication = requestCanCreateApplication( + request, requestType); + + /* Find an existing application for this request. */ + Application application = getExistingApplication(request, + requestCanCreateApplication); + + if (application != null) { + /* + * There is an existing application. We can use this as long as the + * user not specifically requested to close or restart it. + */ + + final boolean restartApplication = (request + .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null); + final boolean closeApplication = (request + .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null); + + if (restartApplication) { + closeApplication(application, request.getSession(false)); + return createApplication(request); + } else if (closeApplication) { + closeApplication(application, request.getSession(false)); + return null; + } else { + return application; + } + } + + // No existing application was found + + if (requestCanCreateApplication) { + /* + * If the request is such that it should create a new application if + * one as not found, we do that. + */ + return createApplication(request); + } else { + /* + * The application was not found and a new one should not be + * created. Assume the session has expired. + */ + throw new SessionExpiredException(); + } + + } + + /** + * Check if the request should create an application if an existing + * application is not found. + * + * @param request + * @param requestType + * @return true if an application should be created, false otherwise + */ + boolean requestCanCreateApplication(HttpServletRequest request, + RequestType requestType) { + if (requestType == RequestType.UIDL && isRepaintAll(request)) { + /* + * UIDL request contains valid repaintAll=1 event, the user probably + * wants to initiate a new application through a custom index.html + * without using the bootstrap page. + */ + return true; + + } else if (requestType == RequestType.OTHER) { + /* + * I.e URIs that are not application resources or static (theme) + * files. + */ + return true; + + } + + return false; + } + + /** + * Gets resource path using different implementations. Required to + * supporting different servlet container implementations (application + * servers). + * + * @param servletContext + * @param path + * the resource path. + * @return the resource path. + */ + protected static String getResourcePath(ServletContext servletContext, + String path) { + String resultPath = null; + resultPath = servletContext.getRealPath(path); + if (resultPath != null) { + return resultPath; + } else { + try { + final URL url = servletContext.getResource(path); + resultPath = url.getFile(); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.INFO, + "Could not find resource path " + path, e); + } + } + return resultPath; + } + + /** + * Creates a new application and registers it into WebApplicationContext + * (aka session). This is not meant to be overridden. Override + * getNewApplication to create the application instance in a custom way. + * + * @param request + * @return + * @throws ServletException + * @throws MalformedURLException + */ + private Application createApplication(HttpServletRequest request) + throws ServletException, MalformedURLException { + Application newApplication = getNewApplication(request); + + final WebApplicationContext context = getApplicationContext(request + .getSession()); + context.addApplication(newApplication); + + return newApplication; + } + + private void handleServiceException(WrappedHttpServletRequest request, + WrappedHttpServletResponse response, Application application, + Throwable e) throws IOException, ServletException { + // if this was an UIDL request, response UIDL back to client + if (getRequestType(request) == RequestType.UIDL) { + Application.SystemMessages ci = getSystemMessages(); + criticalNotification(request, response, + ci.getInternalErrorCaption(), ci.getInternalErrorMessage(), + null, ci.getInternalErrorURL()); + if (application != null) { + application.getErrorHandler() + .terminalError(new RequestError(e)); + } else { + throw new ServletException(e); + } + } else { + // Re-throw other exceptions + throw new ServletException(e); + } + + } + + /** + * A helper method to strip away characters that might somehow be used for + * XSS attacs. Leaves at least alphanumeric characters intact. Also removes + * eg. ( and ), so values should be safe in javascript too. + * + * @param themeName + * @return + */ + protected static String stripSpecialChars(String themeName) { + StringBuilder sb = new StringBuilder(); + char[] charArray = themeName.toCharArray(); + for (int i = 0; i < charArray.length; i++) { + char c = charArray[i]; + if (!CHAR_BLACKLIST.contains(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + private static final Collection<Character> CHAR_BLACKLIST = new HashSet<Character>( + Arrays.asList(new Character[] { '&', '"', '\'', '<', '>', '(', ')', + ';' })); + + /** + * Returns the default theme. Must never return null. + * + * @return + */ + public static String getDefaultTheme() { + return DEFAULT_THEME_NAME; + } + + void handleServiceSessionExpired(WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException, + ServletException { + + if (isOnUnloadRequest(request)) { + /* + * Request was an unload request (e.g. window close event) and the + * client expects no response if it fails. + */ + return; + } + + try { + Application.SystemMessages ci = getSystemMessages(); + if (getRequestType(request) != RequestType.UIDL) { + // 'plain' http req - e.g. browser reload; + // just go ahead redirect the browser + response.sendRedirect(ci.getSessionExpiredURL()); + } else { + /* + * Invalidate session (weird to have session if we're saying + * that it's expired, and worse: portal integration will fail + * since the session is not created by the portal. + * + * Session must be invalidated before criticalNotification as it + * commits the response. + */ + request.getSession().invalidate(); + + // send uidl redirect + criticalNotification(request, response, + ci.getSessionExpiredCaption(), + ci.getSessionExpiredMessage(), null, + ci.getSessionExpiredURL()); + + } + } catch (SystemMessageException ee) { + throw new ServletException(ee); + } + + } + + private void handleServiceSecurityException( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException, + ServletException { + if (isOnUnloadRequest(request)) { + /* + * Request was an unload request (e.g. window close event) and the + * client expects no response if it fails. + */ + return; + } + + try { + Application.SystemMessages ci = getSystemMessages(); + if (getRequestType(request) != RequestType.UIDL) { + // 'plain' http req - e.g. browser reload; + // just go ahead redirect the browser + response.sendRedirect(ci.getCommunicationErrorURL()); + } else { + // send uidl redirect + criticalNotification(request, response, + ci.getCommunicationErrorCaption(), + ci.getCommunicationErrorMessage(), + INVALID_SECURITY_KEY_MSG, ci.getCommunicationErrorURL()); + /* + * Invalidate session. Portal integration will fail otherwise + * since the session is not created by the portal. + */ + request.getSession().invalidate(); + } + } catch (SystemMessageException ee) { + throw new ServletException(ee); + } + + log("Invalid security key received from " + request.getRemoteHost()); + } + + /** + * Creates a new application for the given request. + * + * @param request + * the HTTP request. + * @return A new Application instance. + * @throws ServletException + */ + protected abstract Application getNewApplication(HttpServletRequest request) + throws ServletException; + + /** + * Starts the application if it is not already running. + * + * @param request + * @param application + * @param webApplicationContext + * @throws ServletException + * @throws MalformedURLException + */ + private void startApplication(HttpServletRequest request, + Application application, WebApplicationContext webApplicationContext) + throws ServletException, MalformedURLException { + + if (!application.isRunning()) { + // Create application + final URL applicationUrl = getApplicationUrl(request); + + // Initial locale comes from the request + Locale locale = request.getLocale(); + application.setLocale(locale); + application.start(new ApplicationStartEvent(applicationUrl, + getDeploymentConfiguration().getInitParameters(), + webApplicationContext, isProductionMode())); + addonContext.applicationStarted(application); + } + } + + /** + * Check if this is a request for a static resource and, if it is, serve the + * resource to the client. + * + * @param request + * @param response + * @return true if a file was served and the request has been handled, false + * otherwise. + * @throws IOException + * @throws ServletException + */ + private boolean serveStaticResources(HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + + // FIXME What does 10 refer to? + String pathInfo = request.getPathInfo(); + if (pathInfo == null || pathInfo.length() <= 10) { + return false; + } + + if ((request.getContextPath() != null) + && (request.getRequestURI().startsWith("/VAADIN/"))) { + serveStaticResourcesInVAADIN(request.getRequestURI(), request, + response); + return true; + } else if (request.getRequestURI().startsWith( + request.getContextPath() + "/VAADIN/")) { + serveStaticResourcesInVAADIN( + request.getRequestURI().substring( + request.getContextPath().length()), request, + response); + return true; + } + + return false; + } + + /** + * Serve resources from VAADIN directory. + * + * @param filename + * The filename to serve. Should always start with /VAADIN/. + * @param request + * @param response + * @throws IOException + * @throws ServletException + */ + private void serveStaticResourcesInVAADIN(String filename, + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + final ServletContext sc = getServletContext(); + URL resourceUrl = sc.getResource(filename); + if (resourceUrl == null) { + // try if requested file is found from classloader + + // strip leading "/" otherwise stream from JAR wont work + filename = filename.substring(1); + resourceUrl = getDeploymentConfiguration().getClassLoader() + .getResource(filename); + + if (resourceUrl == null) { + // cannot serve requested file + getLogger() + .info("Requested resource [" + + filename + + "] not found from filesystem or through class loader." + + " Add widgetset and/or theme JAR to your classpath or add files to WebContent/VAADIN folder."); + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // security check: do not permit navigation out of the VAADIN + // directory + if (!isAllowedVAADINResourceUrl(request, resourceUrl)) { + getLogger() + .info("Requested resource [" + + filename + + "] not accessible in the VAADIN directory or access to it is forbidden."); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + + // Find the modification timestamp + long lastModifiedTime = 0; + URLConnection connection = null; + try { + connection = resourceUrl.openConnection(); + lastModifiedTime = connection.getLastModified(); + // Remove milliseconds to avoid comparison problems (milliseconds + // are not returned by the browser in the "If-Modified-Since" + // header). + lastModifiedTime = lastModifiedTime - lastModifiedTime % 1000; + + if (browserHasNewestVersion(request, lastModifiedTime)) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + } catch (Exception e) { + // Failed to find out last modified timestamp. Continue without it. + getLogger() + .log(Level.FINEST, + "Failed to find out last modified timestamp. Continuing without it.", + e); + } finally { + if (connection instanceof URLConnection) { + try { + // Explicitly close the input stream to prevent it + // from remaining hanging + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700 + InputStream is = connection.getInputStream(); + if (is != null) { + is.close(); + } + } catch (IOException e) { + getLogger().log(Level.INFO, + "Error closing URLConnection input stream", e); + } + } + } + + // Set type mime type if we can determine it based on the filename + final String mimetype = sc.getMimeType(filename); + if (mimetype != null) { + response.setContentType(mimetype); + } + + // Provide modification timestamp to the browser if it is known. + if (lastModifiedTime > 0) { + response.setDateHeader("Last-Modified", lastModifiedTime); + /* + * The browser is allowed to cache for 1 hour without checking if + * the file has changed. This forces browsers to fetch a new version + * when the Vaadin version is updated. This will cause more requests + * to the servlet than without this but for high volume sites the + * static files should never be served through the servlet. The + * cache timeout can be configured by setting the resourceCacheTime + * parameter in web.xml + */ + response.setHeader("Cache-Control", + "max-age= " + String.valueOf(resourceCacheTime)); + } + + // Write the resource to the client. + final OutputStream os = response.getOutputStream(); + final byte buffer[] = new byte[DEFAULT_BUFFER_SIZE]; + int bytes; + InputStream is = resourceUrl.openStream(); + while ((bytes = is.read(buffer)) >= 0) { + os.write(buffer, 0, bytes); + } + is.close(); + } + + /** + * Check whether a URL obtained from a classloader refers to a valid static + * resource in the directory VAADIN. + * + * Warning: Overriding of this method is not recommended, but is possible to + * support non-default classloaders or servers that may produce URLs + * different from the normal ones. The method prototype may change in the + * future. Care should be taken not to expose class files or other resources + * outside the VAADIN directory if the method is overridden. + * + * @param request + * @param resourceUrl + * @return + * + * @since 6.6.7 + */ + protected boolean isAllowedVAADINResourceUrl(HttpServletRequest request, + URL resourceUrl) { + if ("jar".equals(resourceUrl.getProtocol())) { + // This branch is used for accessing resources directly from the + // Vaadin JAR in development environments and in similar cases. + + // Inside a JAR, a ".." would mean a real directory named ".." so + // using it in paths should just result in the file not being found. + // However, performing a check in case some servers or class loaders + // try to normalize the path by collapsing ".." before the class + // loader sees it. + + if (!resourceUrl.getPath().contains("!/VAADIN/")) { + getLogger().info( + "Blocked attempt to access a JAR entry not starting with /VAADIN/: " + + resourceUrl); + return false; + } + getLogger().fine( + "Accepted access to a JAR entry using a class loader: " + + resourceUrl); + return true; + } else { + // Some servers such as GlassFish extract files from JARs (file:) + // and e.g. JBoss 5+ use protocols vsf: and vfsfile: . + + // Check that the URL is in a VAADIN directory and does not contain + // "/../" + if (!resourceUrl.getPath().contains("/VAADIN/") + || resourceUrl.getPath().contains("/../")) { + getLogger().info( + "Blocked attempt to access file: " + resourceUrl); + return false; + } + getLogger().fine( + "Accepted access to a file using a class loader: " + + resourceUrl); + return true; + } + } + + /** + * Checks if the browser has an up to date cached version of requested + * resource. Currently the check is performed using the "If-Modified-Since" + * header. Could be expanded if needed. + * + * @param request + * The HttpServletRequest from the browser. + * @param resourceLastModifiedTimestamp + * The timestamp when the resource was last modified. 0 if the + * last modification time is unknown. + * @return true if the If-Modified-Since header tells the cached version in + * the browser is up to date, false otherwise + */ + private boolean browserHasNewestVersion(HttpServletRequest request, + long resourceLastModifiedTimestamp) { + if (resourceLastModifiedTimestamp < 1) { + // We do not know when it was modified so the browser cannot have an + // up-to-date version + return false; + } + /* + * The browser can request the resource conditionally using an + * If-Modified-Since header. Check this against the last modification + * time. + */ + try { + // If-Modified-Since represents the timestamp of the version cached + // in the browser + long headerIfModifiedSince = request + .getDateHeader("If-Modified-Since"); + + if (headerIfModifiedSince >= resourceLastModifiedTimestamp) { + // Browser has this an up-to-date version of the resource + return true; + } + } catch (Exception e) { + // Failed to parse header. Fail silently - the browser does not have + // an up-to-date version in its cache. + } + return false; + } + + protected enum RequestType { + FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE; + } + + protected RequestType getRequestType(WrappedHttpServletRequest request) { + if (ServletPortletHelper.isFileUploadRequest(request)) { + return RequestType.FILE_UPLOAD; + } else if (ServletPortletHelper.isConnectorResourceRequest(request)) { + return RequestType.CONNECTOR_RESOURCE; + } else if (isBrowserDetailsRequest(request)) { + return RequestType.BROWSER_DETAILS; + } else if (ServletPortletHelper.isUIDLRequest(request)) { + return RequestType.UIDL; + } else if (isStaticResourceRequest(request)) { + return RequestType.STATIC_FILE; + } else if (ServletPortletHelper.isApplicationResourceRequest(request)) { + return RequestType.APPLICATION_RESOURCE; + } + return RequestType.OTHER; + + } + + private static boolean isBrowserDetailsRequest(HttpServletRequest request) { + return "POST".equals(request.getMethod()) + && request.getParameter("browserDetails") != null; + } + + private boolean isStaticResourceRequest(HttpServletRequest request) { + String pathInfo = request.getPathInfo(); + if (pathInfo == null || pathInfo.length() <= 10) { + return false; + } + + if ((request.getContextPath() != null) + && (request.getRequestURI().startsWith("/VAADIN/"))) { + return true; + } else if (request.getRequestURI().startsWith( + request.getContextPath() + "/VAADIN/")) { + return true; + } + + return false; + } + + private boolean isOnUnloadRequest(HttpServletRequest request) { + return request.getParameter(ApplicationConnection.PARAM_UNLOADBURST) != null; + } + + /** + * Get system messages from the current application class + * + * @return + */ + protected SystemMessages getSystemMessages() { + Class<? extends Application> appCls = null; + try { + appCls = getApplicationClass(); + } catch (ClassNotFoundException e) { + // Previous comment claimed that this should never happen + throw new SystemMessageException(e); + } + return getSystemMessages(appCls); + } + + public static SystemMessages getSystemMessages( + Class<? extends Application> appCls) { + try { + if (appCls != null) { + Method m = appCls + .getMethod("getSystemMessages", (Class[]) null); + return (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } + } catch (SecurityException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (NoSuchMethodException e) { + // This is completely ok and should be silently ignored + } catch (IllegalArgumentException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (IllegalAccessException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (InvocationTargetException e) { + // This should never happen + throw new SystemMessageException(e); + } + return Application.getSystemMessages(); + } + + protected abstract Class<? extends Application> getApplicationClass() + throws ClassNotFoundException; + + /** + * Return the URL from where static files, e.g. the widgetset and the theme, + * are served. In a standard configuration the VAADIN folder inside the + * returned folder is what is used for widgetsets and themes. + * + * The returned folder is usually the same as the context path and + * independent of the application. + * + * @param request + * @return The location of static resources (should contain the VAADIN + * directory). Never ends with a slash (/). + */ + protected String getStaticFilesLocation(HttpServletRequest request) { + + return getWebApplicationsStaticFileLocation(request); + } + + /** + * The default method to fetch static files location (URL). This method does + * not check for request attribute {@value #REQUEST_VAADIN_STATIC_FILE_PATH} + * + * @param request + * @return + */ + private String getWebApplicationsStaticFileLocation( + HttpServletRequest request) { + String staticFileLocation; + // if property is defined in configurations, use that + staticFileLocation = getDeploymentConfiguration() + .getApplicationOrSystemProperty(PARAMETER_VAADIN_RESOURCES, + null); + if (staticFileLocation != null) { + return staticFileLocation; + } + + // the last (but most common) option is to generate default location + // from request + + // if context is specified add it to widgetsetUrl + String ctxPath = request.getContextPath(); + + // FIXME: ctxPath.length() == 0 condition is probably unnecessary and + // might even be wrong. + + if (ctxPath.length() == 0 + && request.getAttribute("javax.servlet.include.context_path") != null) { + // include request (e.g portlet), get context path from + // attribute + ctxPath = (String) request + .getAttribute("javax.servlet.include.context_path"); + } + + // Remove heading and trailing slashes from the context path + ctxPath = removeHeadingOrTrailing(ctxPath, "/"); + + if (ctxPath.equals("")) { + return ""; + } else { + return "/" + ctxPath; + } + } + + /** + * Remove any heading or trailing "what" from the "string". + * + * @param string + * @param what + * @return + */ + private static String removeHeadingOrTrailing(String string, String what) { + while (string.startsWith(what)) { + string = string.substring(1); + } + + while (string.endsWith(what)) { + string = string.substring(0, string.length() - 1); + } + + return string; + } + + /** + * Write a redirect response to the main page of the application. + * + * @param request + * @param response + * @throws IOException + * if sending the redirect fails due to an input/output error or + * a bad application URL + */ + private void redirectToApplication(HttpServletRequest request, + HttpServletResponse response) throws IOException { + String applicationUrl = getApplicationUrl(request).toExternalForm(); + response.sendRedirect(response.encodeRedirectURL(applicationUrl)); + } + + /** + * Gets the current application URL from request. + * + * @param request + * the HTTP request. + * @throws MalformedURLException + * if the application is denied access to the persistent data + * store represented by the given URL. + */ + protected URL getApplicationUrl(HttpServletRequest request) + throws MalformedURLException { + final URL reqURL = new URL( + (request.isSecure() ? "https://" : "http://") + + request.getServerName() + + ((request.isSecure() && request.getServerPort() == 443) + || (!request.isSecure() && request + .getServerPort() == 80) ? "" : ":" + + request.getServerPort()) + + request.getRequestURI()); + String servletPath = ""; + if (request.getAttribute("javax.servlet.include.servlet_path") != null) { + // this is an include request + servletPath = request.getAttribute( + "javax.servlet.include.context_path").toString() + + request + .getAttribute("javax.servlet.include.servlet_path"); + + } else { + servletPath = request.getContextPath() + request.getServletPath(); + } + + if (servletPath.length() == 0 + || servletPath.charAt(servletPath.length() - 1) != '/') { + servletPath = servletPath + "/"; + } + URL u = new URL(reqURL, servletPath); + return u; + } + + /** + * Gets the existing application for given request. Looks for application + * instance for given request based on the requested URL. + * + * @param request + * the HTTP request. + * @param allowSessionCreation + * true if a session should be created if no session exists, + * false if no session should be created + * @return Application instance, or null if the URL does not map to valid + * application. + * @throws MalformedURLException + * if the application is denied access to the persistent data + * store represented by the given URL. + * @throws IllegalAccessException + * @throws InstantiationException + * @throws SessionExpiredException + */ + protected Application getExistingApplication(HttpServletRequest request, + boolean allowSessionCreation) throws MalformedURLException, + SessionExpiredException { + + // Ensures that the session is still valid + final HttpSession session = request.getSession(allowSessionCreation); + if (session == null) { + throw new SessionExpiredException(); + } + + WebApplicationContext context = getApplicationContext(session); + + // Gets application list for the session. + final Collection<Application> applications = context.getApplications(); + + // Search for the application (using the application URI) from the list + for (final Iterator<Application> i = applications.iterator(); i + .hasNext();) { + final Application sessionApplication = i.next(); + final String sessionApplicationPath = sessionApplication.getURL() + .getPath(); + String requestApplicationPath = getApplicationUrl(request) + .getPath(); + + if (requestApplicationPath.equals(sessionApplicationPath)) { + // Found a running application + if (sessionApplication.isRunning()) { + return sessionApplication; + } + // Application has stopped, so remove it before creating a new + // application + getApplicationContext(session).removeApplication( + sessionApplication); + break; + } + } + + // Existing application not found + return null; + } + + /** + * Ends the application. + * + * @param request + * the HTTP request. + * @param response + * the HTTP response to write to. + * @param application + * the application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(HttpServletRequest request, + HttpServletResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + + final HttpSession session = request.getSession(); + if (session != null) { + getApplicationContext(session).removeApplication(application); + } + + response.sendRedirect(response.encodeRedirectURL(logoutUrl)); + } + + /** + * Returns the path info; note that this _can_ be different than + * request.getPathInfo(). Examples where this might be useful: + * <ul> + * <li>An application runner servlet that runs different Vaadin applications + * based on an identifier.</li> + * <li>Providing a REST interface in the context root, while serving a + * Vaadin UI on a sub-URI using only one servlet (e.g. REST on + * http://example.com/foo, UI on http://example.com/foo/vaadin)</li> + * + * @param request + * @return + */ + protected String getRequestPathInfo(HttpServletRequest request) { + return request.getPathInfo(); + } + + /** + * Gets relative location of a theme resource. + * + * @param theme + * the Theme name. + * @param resource + * the Theme resource. + * @return External URI specifying the resource + */ + public String getResourceLocation(String theme, ThemeResource resource) { + + if (resourcePath == null) { + return resource.getResourceId(); + } + return resourcePath + theme + "/" + resource.getResourceId(); + } + + private boolean isRepaintAll(HttpServletRequest request) { + return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null) + && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1")); + } + + private void closeApplication(Application application, HttpSession session) { + if (application == null) { + return; + } + + application.close(); + if (session != null) { + WebApplicationContext context = getApplicationContext(session); + context.removeApplication(application); + } + } + + /** + * + * Gets the application context from an HttpSession. If no context is + * currently stored in a session a new context is created and stored in the + * session. + * + * @param session + * the HTTP session. + * @return the application context for HttpSession. + */ + protected WebApplicationContext getApplicationContext(HttpSession session) { + /* + * TODO the ApplicationContext.getApplicationContext() should be removed + * and logic moved here. Now overriding context type is possible, but + * the whole creation logic should be here. MT 1101 + */ + return WebApplicationContext.getApplicationContext(session); + } + + public class RequestError implements Terminal.ErrorEvent, Serializable { + + private final Throwable throwable; + + public RequestError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Override this method if you need to use a specialized communicaiton + * mananger implementation. + * + * @deprecated Instead of overriding this method, override + * {@link WebApplicationContext} implementation via + * {@link AbstractApplicationServlet#getApplicationContext(HttpSession)} + * method and in that customized implementation return your + * CommunicationManager in + * {@link WebApplicationContext#getApplicationManager(Application, AbstractApplicationServlet)} + * method. + * + * @param application + * @return + */ + @Deprecated + public CommunicationManager createCommunicationManager( + Application application) { + return new CommunicationManager(application); + } + + /** + * Escapes characters to html entities. An exception is made for some + * "safe characters" to keep the text somewhat readable. + * + * @param unsafe + * @return a safe string to be added inside an html tag + */ + public static final String safeEscapeForHtml(String unsafe) { + if (null == unsafe) { + return null; + } + StringBuilder safe = new StringBuilder(); + char[] charArray = unsafe.toCharArray(); + for (int i = 0; i < charArray.length; i++) { + char c = charArray[i]; + if (isSafe(c)) { + safe.append(c); + } else { + safe.append("&#"); + safe.append((int) c); + safe.append(";"); + } + } + + return safe.toString(); + } + + private static boolean isSafe(char c) { + return // + c > 47 && c < 58 || // alphanum + c > 64 && c < 91 || // A-Z + c > 96 && c < 123 // a-z + ; + } + + private static final Logger getLogger() { + return Logger.getLogger(AbstractApplicationServlet.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java new file mode 100644 index 0000000000..ba1b3cadb6 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -0,0 +1,2790 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.text.CharacterIterator; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; +import com.vaadin.Application.SystemMessages; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.Version; +import com.vaadin.annotations.JavaScript; +import com.vaadin.annotations.StyleSheet; +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.StreamVariable; +import com.vaadin.terminal.StreamVariable.StreamingEndEvent; +import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; +import com.vaadin.terminal.Terminal.ErrorEvent; +import com.vaadin.terminal.Terminal.ErrorListener; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; +import com.vaadin.terminal.gwt.server.RpcManager.RpcInvocationException; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.AbstractField; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.Root; +import com.vaadin.ui.Window; + +/** + * This is a common base class for the server-side implementations of the + * communication system between the client code (compiled with GWT into + * JavaScript) and the server side components. Its client side counterpart is + * {@link ApplicationConnection}. + * + * TODO Document better! + */ +@SuppressWarnings("serial") +public abstract class AbstractCommunicationManager implements Serializable { + + private static final String DASHDASH = "--"; + + private static final RequestHandler APP_RESOURCE_HANDLER = new ApplicationResourceHandler(); + + private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler(); + + /** + * TODO Document me! + * + * @author peholmst + */ + public interface Callback extends Serializable { + + public void criticalNotification(WrappedRequest request, + WrappedResponse response, String cap, String msg, + String details, String outOfSyncURL) throws IOException; + } + + static class UploadInterruptedException extends Exception { + public UploadInterruptedException() { + super("Upload interrupted by other thread"); + } + } + + private static String GET_PARAM_REPAINT_ALL = "repaintAll"; + + // flag used in the request to indicate that the security token should be + // written to the response + private static final String WRITE_SECURITY_TOKEN_FLAG = "writeSecurityToken"; + + /* Variable records indexes */ + public static final char VAR_BURST_SEPARATOR = '\u001d'; + + public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + + private final HashMap<Integer, ClientCache> rootToClientCache = new HashMap<Integer, ClientCache>(); + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + /* Same as in apache commons file upload library that was previously used. */ + private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; + + private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; + + /** + * The application this communication manager is used for + */ + private final Application application; + + private List<String> locales; + + private int pendingLocalesIndex; + + private int timeoutInterval = -1; + + private DragAndDropService dragAndDropService; + + private String requestThemeName; + + private int maxInactiveInterval; + + private Connector highlightedConnector; + + private Map<String, Class<?>> connectorResourceContexts = new HashMap<String, Class<?>>(); + + private Map<String, Map<String, StreamVariable>> pidToNameToStreamVariable; + + private Map<StreamVariable, String> streamVariableToSeckey; + + /** + * TODO New constructor - document me! + * + * @param application + */ + public AbstractCommunicationManager(Application application) { + this.application = application; + application.addRequestHandler(getBootstrapHandler()); + application.addRequestHandler(APP_RESOURCE_HANDLER); + application.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER); + requireLocale(application.getLocale().toString()); + } + + protected Application getApplication() { + return application; + } + + private static final int LF = "\n".getBytes()[0]; + + private static final String CRLF = "\r\n"; + + private static final String UTF8 = "UTF8"; + + private static final String GET_PARAM_HIGHLIGHT_COMPONENT = "highlightComponent"; + + private static String readLine(InputStream stream) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + int readByte = stream.read(); + while (readByte != LF) { + bout.write(readByte); + readByte = stream.read(); + } + byte[] bytes = bout.toByteArray(); + return new String(bytes, 0, bytes.length - 1, UTF8); + } + + /** + * Method used to stream content from a multipart request (either from + * servlet or portlet request) to given StreamVariable + * + * + * @param request + * @param response + * @param streamVariable + * @param owner + * @param boundary + * @throws IOException + */ + protected void doHandleSimpleMultipartFileUpload(WrappedRequest request, + WrappedResponse response, StreamVariable streamVariable, + String variableName, ClientConnector owner, String boundary) + throws IOException { + // multipart parsing, supports only one file for request, but that is + // fine for our current terminal + + final InputStream inputStream = request.getInputStream(); + + int contentLength = request.getContentLength(); + + boolean atStart = false; + boolean firstFileFieldFound = false; + + String rawfilename = "unknown"; + String rawMimeType = "application/octet-stream"; + + /* + * Read the stream until the actual file starts (empty line). Read + * filename and content type from multipart headers. + */ + while (!atStart) { + String readLine = readLine(inputStream); + contentLength -= (readLine.length() + 2); + if (readLine.startsWith("Content-Disposition:") + && readLine.indexOf("filename=") > 0) { + rawfilename = readLine.replaceAll(".*filename=", ""); + String parenthesis = rawfilename.substring(0, 1); + rawfilename = rawfilename.substring(1); + rawfilename = rawfilename.substring(0, + rawfilename.indexOf(parenthesis)); + firstFileFieldFound = true; + } else if (firstFileFieldFound && readLine.equals("")) { + atStart = true; + } else if (readLine.startsWith("Content-Type")) { + rawMimeType = readLine.split(": ")[1]; + } + } + + contentLength -= (boundary.length() + CRLF.length() + 2 + * DASHDASH.length() + 2); // 2 == CRLF + + /* + * Reads bytes from the underlying stream. Compares the read bytes to + * the boundary string and returns -1 if met. + * + * The matching happens so that if the read byte equals to the first + * char of boundary string, the stream goes to "buffering mode". In + * buffering mode bytes are read until the character does not match the + * corresponding from boundary string or the full boundary string is + * found. + * + * Note, if this is someday needed elsewhere, don't shoot yourself to + * foot and split to a top level helper class. + */ + InputStream simpleMultiPartReader = new SimpleMultiPartInputStream( + inputStream, boundary); + + /* + * Should report only the filename even if the browser sends the path + */ + final String filename = removePath(rawfilename); + final String mimeType = rawMimeType; + + try { + // TODO Shouldn't this check connectorEnabled? + if (owner == null) { + throw new UploadException( + "File upload ignored because the connector for the stream variable was not found"); + } + if (owner instanceof Component) { + if (((Component) owner).isReadOnly()) { + throw new UploadException( + "Warning: file upload ignored because the componente was read-only"); + } + } + boolean forgetVariable = streamToReceiver(simpleMultiPartReader, + streamVariable, filename, mimeType, contentLength); + if (forgetVariable) { + cleanStreamVariable(owner, variableName); + } + } catch (Exception e) { + synchronized (application) { + handleChangeVariablesError(application, (Component) owner, e, + new HashMap<String, Object>()); + } + } + sendUploadResponse(request, response); + + } + + /** + * Used to stream plain file post (aka XHR2.post(File)) + * + * @param request + * @param response + * @param streamVariable + * @param owner + * @param contentLength + * @throws IOException + */ + protected void doHandleXhrFilePost(WrappedRequest request, + WrappedResponse response, StreamVariable streamVariable, + String variableName, ClientConnector owner, int contentLength) + throws IOException { + + // These are unknown in filexhr ATM, maybe add to Accept header that + // is accessible in portlets + final String filename = "unknown"; + final String mimeType = filename; + final InputStream stream = request.getInputStream(); + try { + /* + * safe cast as in GWT terminal all variable owners are expected to + * be components. + */ + Component component = (Component) owner; + if (component.isReadOnly()) { + throw new UploadException( + "Warning: file upload ignored because the component was read-only"); + } + boolean forgetVariable = streamToReceiver(stream, streamVariable, + filename, mimeType, contentLength); + if (forgetVariable) { + cleanStreamVariable(owner, variableName); + } + } catch (Exception e) { + synchronized (application) { + handleChangeVariablesError(application, (Component) owner, e, + new HashMap<String, Object>()); + } + } + sendUploadResponse(request, response); + } + + /** + * @param in + * @param streamVariable + * @param filename + * @param type + * @param contentLength + * @return true if the streamvariable has informed that the terminal can + * forget this variable + * @throws UploadException + */ + protected final boolean streamToReceiver(final InputStream in, + StreamVariable streamVariable, String filename, String type, + int contentLength) throws UploadException { + if (streamVariable == null) { + throw new IllegalStateException( + "StreamVariable for the post not found"); + } + + final Application application = getApplication(); + + OutputStream out = null; + int totalBytes = 0; + StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( + filename, type, contentLength); + try { + boolean listenProgress; + synchronized (application) { + streamVariable.streamingStarted(startedEvent); + out = streamVariable.getOutputStream(); + listenProgress = streamVariable.listenProgress(); + } + + // Gets the output target stream + if (out == null) { + throw new NoOutputStreamException(); + } + + if (null == in) { + // No file, for instance non-existent filename in html upload + throw new NoInputStreamException(); + } + + final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE]; + int bytesReadToBuffer = 0; + while ((bytesReadToBuffer = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesReadToBuffer); + totalBytes += bytesReadToBuffer; + if (listenProgress) { + // update progress if listener set and contentLength + // received + synchronized (application) { + StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( + filename, type, contentLength, totalBytes); + streamVariable.onProgress(progressEvent); + } + } + if (streamVariable.isInterrupted()) { + throw new UploadInterruptedException(); + } + } + + // upload successful + out.close(); + StreamingEndEvent event = new StreamingEndEventImpl(filename, type, + totalBytes); + synchronized (application) { + streamVariable.streamingFinished(event); + } + + } catch (UploadInterruptedException e) { + // Download interrupted by application code + tryToCloseStream(out); + StreamingErrorEvent event = new StreamingErrorEventImpl(filename, + type, contentLength, totalBytes, e); + synchronized (application) { + streamVariable.streamingFailed(event); + } + // Note, we are not throwing interrupted exception forward as it is + // not a terminal level error like all other exception. + } catch (final Exception e) { + tryToCloseStream(out); + synchronized (application) { + StreamingErrorEvent event = new StreamingErrorEventImpl( + filename, type, contentLength, totalBytes, e); + synchronized (application) { + streamVariable.streamingFailed(event); + } + // throw exception for terminal to be handled (to be passed to + // terminalErrorHandler) + throw new UploadException(e); + } + } + return startedEvent.isDisposed(); + } + + static void tryToCloseStream(OutputStream out) { + try { + // try to close output stream (e.g. file handle) + if (out != null) { + out.close(); + } + } catch (IOException e1) { + // NOP + } + } + + /** + * Removes any possible path information from the filename and returns the + * filename. Separators / and \\ are used. + * + * @param name + * @return + */ + private static String removePath(String filename) { + if (filename != null) { + filename = filename.replaceAll("^.*[/\\\\]", ""); + } + + return filename; + } + + /** + * TODO document + * + * @param request + * @param response + * @throws IOException + */ + protected void sendUploadResponse(WrappedRequest request, + WrappedResponse response) throws IOException { + response.setContentType("text/html"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>download handled</body></html>"); + outWriter.flush(); + out.close(); + } + + /** + * Internally process a UIDL request from the client. + * + * This method calls + * {@link #handleVariables(WrappedRequest, WrappedResponse, Callback, Application, Root)} + * to process any changes to variables by the client and then repaints + * affected components using {@link #paintAfterVariableChanges()}. + * + * Also, some cleanup is done when a request arrives for an application that + * has already been closed. + * + * The method handleUidlRequest(...) in subclasses should call this method. + * + * TODO better documentation + * + * @param request + * @param response + * @param callback + * @param root + * target window for the UIDL request, can be null if target not + * found + * @throws IOException + * @throws InvalidUIDLSecurityKeyException + * @throws JSONException + */ + public void handleUidlRequest(WrappedRequest request, + WrappedResponse response, Callback callback, Root root) + throws IOException, InvalidUIDLSecurityKeyException, JSONException { + + checkWidgetsetVersion(request); + requestThemeName = request.getParameter("theme"); + maxInactiveInterval = request.getSessionMaxInactiveInterval(); + // repaint requested or session has timed out and new one is created + boolean repaintAll; + final OutputStream out; + + repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null); + // || (request.getSession().isNew()); FIXME What the h*ll is this?? + out = response.getOutputStream(); + + boolean analyzeLayouts = false; + if (repaintAll) { + // analyzing can be done only with repaintAll + analyzeLayouts = (request.getParameter(GET_PARAM_ANALYZE_LAYOUTS) != null); + + if (request.getParameter(GET_PARAM_HIGHLIGHT_COMPONENT) != null) { + String pid = request + .getParameter(GET_PARAM_HIGHLIGHT_COMPONENT); + highlightedConnector = root.getConnectorTracker().getConnector( + pid); + highlightConnector(highlightedConnector); + } + } + + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + // The rest of the process is synchronized with the application + // in order to guarantee that no parallel variable handling is + // made + synchronized (application) { + + // Finds the window within the application + if (application.isRunning()) { + // Returns if no window found + if (root == null) { + // This should not happen, no windows exists but + // application is still open. + getLogger().warning("Could not get root for application"); + return; + } + } else { + // application has been closed + endApplication(request, response, application); + return; + } + + // Change all variables based on request parameters + if (!handleVariables(request, response, callback, application, root)) { + + // var inconsistency; the client is probably out-of-sync + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod( + "getSystemMessages", (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } catch (Exception e2) { + // FIXME: Handle exception + // Not critical, but something is still wrong; print + // stacktrace + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e2); + } + if (ci != null) { + String msg = ci.getOutOfSyncMessage(); + String cap = ci.getOutOfSyncCaption(); + if (msg != null || cap != null) { + callback.criticalNotification(request, response, cap, + msg, null, ci.getOutOfSyncURL()); + // will reload page after this + return; + } + } + // No message to show, let's just repaint all. + repaintAll = true; + } + + paintAfterVariableChanges(request, response, callback, repaintAll, + outWriter, root, analyzeLayouts); + postPaint(root); + } + + outWriter.close(); + requestThemeName = null; + } + + /** + * Checks that the version reported by the client (widgetset) matches that + * of the server. + * + * @param request + */ + private void checkWidgetsetVersion(WrappedRequest request) { + String widgetsetVersion = request.getParameter("wsver"); + if (widgetsetVersion == null) { + // Only check when the widgetset version is reported. It is reported + // in the first UIDL request (not the initial request as it is a + // plain GET /) + return; + } + + if (!Version.getFullVersion().equals(widgetsetVersion)) { + getLogger().warning( + String.format(Constants.WIDGETSET_MISMATCH_INFO, + Version.getFullVersion(), widgetsetVersion)); + } + } + + /** + * Method called after the paint phase while still being synchronized on the + * application + * + * @param root + * + */ + protected void postPaint(Root root) { + // Remove connectors that have been detached from the application during + // handling of the request + root.getConnectorTracker().cleanConnectorMap(); + + if (pidToNameToStreamVariable != null) { + Iterator<String> iterator = pidToNameToStreamVariable.keySet() + .iterator(); + while (iterator.hasNext()) { + String connectorId = iterator.next(); + if (root.getConnectorTracker().getConnector(connectorId) == null) { + // Owner is no longer attached to the application + Map<String, StreamVariable> removed = pidToNameToStreamVariable + .get(connectorId); + for (String key : removed.keySet()) { + streamVariableToSeckey.remove(removed.get(key)); + } + iterator.remove(); + } + } + } + } + + protected void highlightConnector(Connector highlightedConnector) { + StringBuilder sb = new StringBuilder(); + sb.append("*** Debug details of a component: *** \n"); + sb.append("Type: "); + sb.append(highlightedConnector.getClass().getName()); + if (highlightedConnector instanceof AbstractComponent) { + AbstractComponent component = (AbstractComponent) highlightedConnector; + sb.append("\nId:"); + sb.append(highlightedConnector.getConnectorId()); + if (component.getCaption() != null) { + sb.append("\nCaption:"); + sb.append(component.getCaption()); + } + + printHighlightedComponentHierarchy(sb, component); + } + getLogger().info(sb.toString()); + } + + protected void printHighlightedComponentHierarchy(StringBuilder sb, + AbstractComponent component) { + LinkedList<Component> h = new LinkedList<Component>(); + h.add(component); + Component parent = component.getParent(); + while (parent != null) { + h.addFirst(parent); + parent = parent.getParent(); + } + + sb.append("\nComponent hierarchy:\n"); + Application application2 = component.getApplication(); + sb.append(application2.getClass().getName()); + sb.append("."); + sb.append(application2.getClass().getSimpleName()); + sb.append("("); + sb.append(application2.getClass().getSimpleName()); + sb.append(".java"); + sb.append(":1)"); + int l = 1; + for (Component component2 : h) { + sb.append("\n"); + for (int i = 0; i < l; i++) { + sb.append(" "); + } + l++; + Class<? extends Component> componentClass = component2.getClass(); + Class<?> topClass = componentClass; + while (topClass.getEnclosingClass() != null) { + topClass = topClass.getEnclosingClass(); + } + sb.append(componentClass.getName()); + sb.append("."); + sb.append(componentClass.getSimpleName()); + sb.append("("); + sb.append(topClass.getSimpleName()); + sb.append(".java:1)"); + } + } + + /** + * TODO document + * + * @param request + * @param response + * @param callback + * @param repaintAll + * @param outWriter + * @param window + * @param analyzeLayouts + * @throws PaintException + * @throws IOException + * @throws JSONException + */ + private void paintAfterVariableChanges(WrappedRequest request, + WrappedResponse response, Callback callback, boolean repaintAll, + final PrintWriter outWriter, Root root, boolean analyzeLayouts) + throws PaintException, IOException, JSONException { + + // Removes application if it has stopped during variable changes + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + openJsonMessage(outWriter, response); + + // security key + Object writeSecurityTokenFlag = request + .getAttribute(WRITE_SECURITY_TOKEN_FLAG); + + if (writeSecurityTokenFlag != null) { + outWriter.print(getSecurityKeyUIDL(request)); + } + + writeUidlResponse(request, repaintAll, outWriter, root, analyzeLayouts); + + closeJsonMessage(outWriter); + + outWriter.close(); + + } + + /** + * Gets the security key (and generates one if needed) as UIDL. + * + * @param request + * @return the security key UIDL or "" if the feature is turned off + */ + public String getSecurityKeyUIDL(WrappedRequest request) { + final String seckey = getSecurityKey(request); + if (seckey != null) { + return "\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID + + "\":\"" + seckey + "\","; + } else { + return ""; + } + } + + /** + * Gets the security key (and generates one if needed). + * + * @param request + * @return the security key + */ + protected String getSecurityKey(WrappedRequest request) { + String seckey = null; + seckey = (String) request + .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); + if (seckey == null) { + seckey = UUID.randomUUID().toString(); + request.setSessionAttribute( + ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey); + } + + return seckey; + } + + @SuppressWarnings("unchecked") + public void writeUidlResponse(WrappedRequest request, boolean repaintAll, + final PrintWriter outWriter, Root root, boolean analyzeLayouts) + throws PaintException, JSONException { + ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>(); + Application application = root.getApplication(); + // Paints components + ConnectorTracker rootConnectorTracker = root.getConnectorTracker(); + getLogger().log(Level.FINE, "* Creating response to client"); + if (repaintAll) { + getClientCache(root).clear(); + rootConnectorTracker.markAllConnectorsDirty(); + + // Reset sent locales + locales = null; + requireLocale(application.getLocale().toString()); + } + + dirtyVisibleConnectors + .addAll(getDirtyVisibleConnectors(rootConnectorTracker)); + + getLogger().log( + Level.FINE, + "Found " + dirtyVisibleConnectors.size() + + " dirty connectors to paint"); + for (ClientConnector connector : dirtyVisibleConnectors) { + if (connector instanceof Component) { + ((Component) connector).updateState(); + } + } + rootConnectorTracker.markAllConnectorsClean(); + + outWriter.print("\"changes\":["); + + List<InvalidLayout> invalidComponentRelativeSizes = null; + + JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, + !repaintAll); + legacyPaint(paintTarget, dirtyVisibleConnectors); + + if (analyzeLayouts) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes(root.getContent(), null, + null); + + // Also check any existing subwindows + if (root.getWindows() != null) { + for (Window subWindow : root.getWindows()) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes( + subWindow.getContent(), + invalidComponentRelativeSizes, null); + } + } + } + + paintTarget.close(); + outWriter.print("], "); // close changes + + // send shared state to client + + // for now, send the complete state of all modified and new + // components + + // Ideally, all this would be sent before "changes", but that causes + // complications with legacy components that create sub-components + // in their paint phase. Nevertheless, this will be processed on the + // client after component creation but before legacy UIDL + // processing. + JSONObject sharedStates = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + SharedState state = connector.getState(); + if (null != state) { + // encode and send shared state + try { + Class<? extends SharedState> stateType = connector + .getStateType(); + SharedState referenceState = null; + if (repaintAll) { + // Use an empty state object as reference for full + // repaints + try { + referenceState = stateType.newInstance(); + } catch (Exception e) { + getLogger().log( + Level.WARNING, + "Error creating reference object for state of type " + + stateType.getName()); + } + } + Object stateJson = JsonCodec.encode(state, referenceState, + stateType, root.getConnectorTracker()); + + sharedStates.put(connector.getConnectorId(), stateJson); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize shared state for connector " + + connector.getClass().getName() + " (" + + connector.getConnectorId() + "): " + + e.getMessage(), e); + } + } + } + outWriter.print("\"state\":"); + outWriter.append(sharedStates.toString()); + outWriter.print(", "); // close states + + // TODO This should be optimized. The type only needs to be + // sent once for each connector id + on refresh. Use the same cache as + // widget mapping + + JSONObject connectorTypes = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorType = paintTarget.getTag(connector); + try { + connectorTypes.put(connector.getConnectorId(), connectorType); + } catch (JSONException e) { + throw new PaintException( + "Failed to send connector type for connector " + + connector.getConnectorId() + ": " + + e.getMessage(), e); + } + } + outWriter.print("\"types\":"); + outWriter.append(connectorTypes.toString()); + outWriter.print(", "); // close states + + // Send update hierarchy information to the client. + + // This could be optimized aswell to send only info if hierarchy has + // actually changed. Much like with the shared state. Note though + // that an empty hierarchy is information aswell (e.g. change from 1 + // child to 0 children) + + outWriter.print("\"hierarchy\":"); + + JSONObject hierarchyInfo = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorId = connector.getConnectorId(); + JSONArray children = new JSONArray(); + + for (ClientConnector child : AbstractClientConnector + .getAllChildrenIterable(connector)) { + if (isVisible(child)) { + children.put(child.getConnectorId()); + } + } + try { + hierarchyInfo.put(connectorId, children); + } catch (JSONException e) { + throw new PaintException( + "Failed to send hierarchy information about " + + connectorId + " to the client: " + + e.getMessage(), e); + } + } + outWriter.append(hierarchyInfo.toString()); + outWriter.print(", "); // close hierarchy + + // send server to client RPC calls for components in the root, in call + // order + + // collect RPC calls from components in the root in the order in + // which they were performed, remove the calls from components + + LinkedList<ClientConnector> rpcPendingQueue = new LinkedList<ClientConnector>( + dirtyVisibleConnectors); + List<ClientMethodInvocation> pendingInvocations = collectPendingRpcCalls(dirtyVisibleConnectors); + + JSONArray rpcCalls = new JSONArray(); + for (ClientMethodInvocation invocation : pendingInvocations) { + // add invocation to rpcCalls + try { + JSONArray invocationJson = new JSONArray(); + invocationJson.put(invocation.getConnector().getConnectorId()); + invocationJson.put(invocation.getInterfaceName()); + invocationJson.put(invocation.getMethodName()); + JSONArray paramJson = new JSONArray(); + for (int i = 0; i < invocation.getParameterTypes().length; ++i) { + Type parameterType = invocation.getParameterTypes()[i]; + Object referenceParameter = null; + // TODO Use default values for RPC parameter types + // if (!JsonCodec.isInternalType(parameterType)) { + // try { + // referenceParameter = parameterType.newInstance(); + // } catch (Exception e) { + // logger.log(Level.WARNING, + // "Error creating reference object for parameter of type " + // + parameterType.getName()); + // } + // } + paramJson.put(JsonCodec.encode( + invocation.getParameters()[i], referenceParameter, + parameterType, root.getConnectorTracker())); + } + invocationJson.put(paramJson); + rpcCalls.put(invocationJson); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize RPC method call parameters for connector " + + invocation.getConnector().getConnectorId() + + " method " + invocation.getInterfaceName() + + "." + invocation.getMethodName() + ": " + + e.getMessage(), e); + } + + } + + if (rpcCalls.length() > 0) { + outWriter.print("\"rpc\" : "); + outWriter.append(rpcCalls.toString()); + outWriter.print(", "); // close rpc + } + + outWriter.print("\"meta\" : {"); + boolean metaOpen = false; + + if (repaintAll) { + metaOpen = true; + outWriter.write("\"repaintAll\":true"); + if (analyzeLayouts) { + outWriter.write(", \"invalidLayouts\":"); + outWriter.write("["); + if (invalidComponentRelativeSizes != null) { + boolean first = true; + for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { + if (!first) { + outWriter.write(","); + } else { + first = false; + } + invalidLayout.reportErrors(outWriter, this, System.err); + } + } + outWriter.write("]"); + } + if (highlightedConnector != null) { + outWriter.write(", \"hl\":\""); + outWriter.write(highlightedConnector.getConnectorId()); + outWriter.write("\""); + highlightedConnector = null; + } + } + + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod("getSystemMessages", + (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, (Object[]) null); + } catch (NoSuchMethodException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } catch (IllegalArgumentException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } catch (IllegalAccessException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } catch (InvocationTargetException e) { + getLogger().log(Level.WARNING, + "getSystemMessages() failed - continuing", e); + } + + // meta instruction for client to enable auto-forward to + // sessionExpiredURL after timer expires. + if (ci != null && ci.getSessionExpiredMessage() == null + && ci.getSessionExpiredCaption() == null + && ci.isSessionExpiredNotificationEnabled()) { + int newTimeoutInterval = getTimeoutInterval(); + if (repaintAll || (timeoutInterval != newTimeoutInterval)) { + String escapedURL = ci.getSessionExpiredURL() == null ? "" : ci + .getSessionExpiredURL().replace("/", "\\/"); + if (metaOpen) { + outWriter.write(","); + } + outWriter.write("\"timedRedirect\":{\"interval\":" + + (newTimeoutInterval + 15) + ",\"url\":\"" + + escapedURL + "\"}"); + metaOpen = true; + } + timeoutInterval = newTimeoutInterval; + } + + outWriter.print("}, \"resources\" : {"); + + // Precache custom layouts + + // TODO We should only precache the layouts that are not + // cached already (plagiate from usedPaintableTypes) + int resourceIndex = 0; + for (final Iterator<Object> i = paintTarget.getUsedResources() + .iterator(); i.hasNext();) { + final String resource = (String) i.next(); + InputStream is = null; + try { + is = getThemeResourceAsStream(root, getTheme(root), resource); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Failed to get theme resource stream.", e); + } + if (is != null) { + + outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\"" + + resource + "\" : "); + final StringBuffer layout = new StringBuffer(); + + try { + final InputStreamReader r = new InputStreamReader(is, + "UTF-8"); + final char[] buffer = new char[20000]; + int charsRead = 0; + while ((charsRead = r.read(buffer)) > 0) { + layout.append(buffer, 0, charsRead); + } + r.close(); + } catch (final java.io.IOException e) { + // FIXME: Handle exception + getLogger().log(Level.INFO, "Resource transfer failed", e); + } + outWriter.print("\"" + + JsonPaintTarget.escapeJSON(layout.toString()) + "\""); + } else { + // FIXME: Handle exception + getLogger().severe("CustomLayout not found: " + resource); + } + } + outWriter.print("}"); + + Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget + .getUsedClientConnectors(); + boolean typeMappingsOpen = false; + ClientCache clientCache = getClientCache(root); + + List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>(); + + for (Class<? extends ClientConnector> class1 : usedClientConnectors) { + if (clientCache.cache(class1)) { + // client does not know the mapping key for this type, send + // mapping to client + newConnectorTypes.add(class1); + + if (!typeMappingsOpen) { + typeMappingsOpen = true; + outWriter.print(", \"typeMappings\" : { "); + } else { + outWriter.print(" , "); + } + String canonicalName = class1.getCanonicalName(); + outWriter.print("\""); + outWriter.print(canonicalName); + outWriter.print("\" : "); + outWriter.print(getTagForType(class1)); + } + } + if (typeMappingsOpen) { + outWriter.print(" }"); + } + + boolean typeInheritanceMapOpen = false; + if (typeMappingsOpen) { + // send the whole type inheritance map if any new mappings + for (Class<? extends ClientConnector> class1 : usedClientConnectors) { + if (!ClientConnector.class.isAssignableFrom(class1 + .getSuperclass())) { + continue; + } + if (!typeInheritanceMapOpen) { + typeInheritanceMapOpen = true; + outWriter.print(", \"typeInheritanceMap\" : { "); + } else { + outWriter.print(" , "); + } + outWriter.print("\""); + outWriter.print(getTagForType(class1)); + outWriter.print("\" : "); + outWriter + .print(getTagForType((Class<? extends ClientConnector>) class1 + .getSuperclass())); + } + if (typeInheritanceMapOpen) { + outWriter.print(" }"); + } + } + + /* + * Ensure super classes come before sub classes to get script dependency + * order right. Sub class @JavaScript might assume that @JavaScript + * defined by super class is already loaded. + */ + Collections.sort(newConnectorTypes, new Comparator<Class<?>>() { + @Override + public int compare(Class<?> o1, Class<?> o2) { + // TODO optimize using Class.isAssignableFrom? + return hierarchyDepth(o1) - hierarchyDepth(o2); + } + + private int hierarchyDepth(Class<?> type) { + if (type == Object.class) { + return 0; + } else { + return hierarchyDepth(type.getSuperclass()) + 1; + } + } + }); + + List<String> scriptDependencies = new ArrayList<String>(); + List<String> styleDependencies = new ArrayList<String>(); + + for (Class<? extends ClientConnector> class1 : newConnectorTypes) { + JavaScript jsAnnotation = class1.getAnnotation(JavaScript.class); + if (jsAnnotation != null) { + for (String resource : jsAnnotation.value()) { + scriptDependencies.add(registerResource(resource, class1)); + } + } + + StyleSheet styleAnnotation = class1.getAnnotation(StyleSheet.class); + if (styleAnnotation != null) { + for (String resource : styleAnnotation.value()) { + styleDependencies.add(registerResource(resource, class1)); + } + } + } + + // Include script dependencies in output if there are any + if (!scriptDependencies.isEmpty()) { + outWriter.print(", \"scriptDependencies\": " + + new JSONArray(scriptDependencies).toString()); + } + + // Include style dependencies in output if there are any + if (!styleDependencies.isEmpty()) { + outWriter.print(", \"styleDependencies\": " + + new JSONArray(styleDependencies).toString()); + } + + // add any pending locale definitions requested by the client + printLocaleDeclarations(outWriter); + + if (dragAndDropService != null) { + dragAndDropService.printJSONResponse(outWriter); + } + + writePerformanceData(outWriter); + } + + /** + * Resolves a resource URI, registering the URI with this + * {@code AbstractCommunicationManager} if needed and returns a fully + * qualified URI. + */ + private String registerResource(String resourceUri, Class<?> context) { + try { + URI uri = new URI(resourceUri); + String protocol = uri.getScheme(); + + if ("connector".equals(protocol)) { + // Strip initial slash + String resourceName = uri.getPath().substring(1); + return registerConnectorResource(resourceName, context); + } + + if (protocol != null || uri.getHost() != null) { + return resourceUri; + } + + // Bare path interpreted as connector resource + return registerConnectorResource(resourceUri, context); + } catch (URISyntaxException e) { + getLogger().log(Level.WARNING, + "Could not parse resource url " + resourceUri, e); + return resourceUri; + } + } + + private String registerConnectorResource(String name, Class<?> context) { + synchronized (connectorResourceContexts) { + // Add to map of names accepted by serveConnectorResource + if (connectorResourceContexts.containsKey(name)) { + Class<?> oldContext = connectorResourceContexts.get(name); + if (oldContext != context) { + getLogger().warning( + "Resource " + name + " defined by both " + context + + " and " + oldContext + ". Resource from " + + oldContext + " will be used."); + } + } else { + connectorResourceContexts.put(name, context); + } + } + + return ApplicationConnection.CONNECTOR_PROTOCOL_PREFIX + "/" + name; + } + + /** + * Adds the performance timing data (used by TestBench 3) to the UIDL + * response. + */ + private void writePerformanceData(final PrintWriter outWriter) { + AbstractWebApplicationContext ctx = (AbstractWebApplicationContext) application + .getContext(); + outWriter.write(String.format(", \"timings\":[%d, %d]", + ctx.getTotalSessionTime(), ctx.getLastRequestTime())); + } + + private void legacyPaint(PaintTarget paintTarget, + ArrayList<ClientConnector> dirtyVisibleConnectors) + throws PaintException { + List<Vaadin6Component> legacyComponents = new ArrayList<Vaadin6Component>(); + for (Connector connector : dirtyVisibleConnectors) { + // All Components that want to use paintContent must implement + // Vaadin6Component + if (connector instanceof Vaadin6Component) { + legacyComponents.add((Vaadin6Component) connector); + } + } + sortByHierarchy((List) legacyComponents); + for (Vaadin6Component c : legacyComponents) { + getLogger().fine( + "Painting Vaadin6Component " + c.getClass().getName() + "@" + + Integer.toHexString(c.hashCode())); + paintTarget.startTag("change"); + final String pid = c.getConnectorId(); + paintTarget.addAttribute("pid", pid); + LegacyPaint.paint(c, paintTarget); + paintTarget.endTag("change"); + } + + } + + private void sortByHierarchy(List<Component> paintables) { + // Vaadin 6 requires parents to be painted before children as component + // containers rely on that their updateFromUIDL method has been called + // before children start calling e.g. updateCaption + Collections.sort(paintables, new Comparator<Component>() { + + @Override + public int compare(Component c1, Component c2) { + int depth1 = 0; + while (c1.getParent() != null) { + depth1++; + c1 = c1.getParent(); + } + int depth2 = 0; + while (c2.getParent() != null) { + depth2++; + c2 = c2.getParent(); + } + if (depth1 < depth2) { + return -1; + } + if (depth1 > depth2) { + return 1; + } + return 0; + } + }); + + } + + private ClientCache getClientCache(Root root) { + Integer rootId = Integer.valueOf(root.getRootId()); + ClientCache cache = rootToClientCache.get(rootId); + if (cache == null) { + cache = new ClientCache(); + rootToClientCache.put(rootId, cache); + } + return cache; + } + + /** + * Checks if the connector is visible in context. For Components, + * {@link #isVisible(Component)} is used. For other types of connectors, the + * contextual visibility of its first Component ancestor is used. If no + * Component ancestor is found, the connector is not visible. + * + * @param connector + * The connector to check + * @return <code>true</code> if the connector is visible to the client, + * <code>false</code> otherwise + */ + static boolean isVisible(ClientConnector connector) { + if (connector instanceof Component) { + return isVisible((Component) connector); + } else { + ClientConnector parent = connector.getParent(); + if (parent == null) { + return false; + } else { + return isVisible(parent); + } + } + } + + /** + * Checks if the component is visible in context, i.e. returns false if the + * child is hidden, the parent is hidden or the parent says the child should + * not be rendered (using + * {@link HasComponents#isComponentVisible(Component)} + * + * @param child + * The child to check + * @return true if the child is visible to the client, false otherwise + */ + static boolean isVisible(Component child) { + if (!child.isVisible()) { + return false; + } + + HasComponents parent = child.getParent(); + if (parent == null) { + if (child instanceof Root) { + return child.isVisible(); + } else { + return false; + } + } + + return parent.isComponentVisible(child) && isVisible(parent); + } + + private static class NullIterator<E> implements Iterator<E> { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public E next() { + return null; + } + + @Override + public void remove() { + } + + } + + /** + * Collects all pending RPC calls from listed {@link ClientConnector}s and + * clears their RPC queues. + * + * @param rpcPendingQueue + * list of {@link ClientConnector} of interest + * @return ordered list of pending RPC calls + */ + private List<ClientMethodInvocation> collectPendingRpcCalls( + List<ClientConnector> rpcPendingQueue) { + List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); + for (ClientConnector connector : rpcPendingQueue) { + List<ClientMethodInvocation> paintablePendingRpc = connector + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List<ClientMethodInvocation> oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList<ClientMethodInvocation>( + totalCalls); + + // merge two ordered comparable lists + for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { + if (paintableIndex >= paintablePendingRpc.size() + || (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc + .get(oldIndex)) + .compareTo(paintablePendingRpc + .get(paintableIndex)) <= 0)) { + pendingInvocations.add(oldPendingRpc.get(oldIndex++)); + } else { + pendingInvocations.add(paintablePendingRpc + .get(paintableIndex++)); + } + } + } + } + return pendingInvocations; + } + + protected abstract InputStream getThemeResourceAsStream(Root root, + String themeName, String resource); + + private int getTimeoutInterval() { + return maxInactiveInterval; + } + + private String getTheme(Root root) { + String themeName = root.getApplication().getThemeForRoot(root); + String requestThemeName = getRequestTheme(); + + if (requestThemeName != null) { + themeName = requestThemeName; + } + if (themeName == null) { + themeName = AbstractApplicationServlet.getDefaultTheme(); + } + return themeName; + } + + private String getRequestTheme() { + return requestThemeName; + } + + /** + * Returns false if the cross site request forgery protection is turned off. + * + * @param application + * @return false if the XSRF is turned off, true otherwise + */ + public boolean isXSRFEnabled(Application application) { + return !"true" + .equals(application + .getProperty(AbstractApplicationServlet.SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION)); + } + + /** + * TODO document + * + * If this method returns false, something was submitted that we did not + * expect; this is probably due to the client being out-of-sync and sending + * variable changes for non-existing pids + * + * @return true if successful, false if there was an inconsistency + */ + private boolean handleVariables(WrappedRequest request, + WrappedResponse response, Callback callback, + Application application2, Root root) throws IOException, + InvalidUIDLSecurityKeyException, JSONException { + boolean success = true; + + String changes = getRequestPayload(request); + if (changes != null) { + + // Manage bursts one by one + final String[] bursts = changes.split(String + .valueOf(VAR_BURST_SEPARATOR)); + + // Security: double cookie submission pattern unless disabled by + // property + if (isXSRFEnabled(application2)) { + if (bursts.length == 1 && "init".equals(bursts[0])) { + // init request; don't handle any variables, key sent in + // response. + request.setAttribute(WRITE_SECURITY_TOKEN_FLAG, true); + return true; + } else { + // ApplicationServlet has stored the security token in the + // session; check that it matched the one sent in the UIDL + String sessId = (String) request + .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); + + if (sessId == null || !sessId.equals(bursts[0])) { + throw new InvalidUIDLSecurityKeyException( + "Security key mismatch"); + } + } + + } + + for (int bi = 1; bi < bursts.length; bi++) { + // unescape any encoded separator characters in the burst + final String burst = unescapeBurst(bursts[bi]); + success &= handleBurst(request, root, burst); + + // In case that there were multiple bursts, we know that this is + // a special synchronous case for closing window. Thus we are + // not interested in sending any UIDL changes back to client. + // Still we must clear component tree between bursts to ensure + // that no removed components are updated. The painting after + // the last burst is handled normally by the calling method. + if (bi < bursts.length - 1) { + + // We will be discarding all changes + final PrintWriter outWriter = new PrintWriter( + new CharArrayWriter()); + + paintAfterVariableChanges(request, response, callback, + true, outWriter, root, false); + + } + + } + } + /* + * Note that we ignore inconsistencies while handling unload request. + * The client can't remove invalid variable changes from the burst, and + * we don't have the required logic implemented on the server side. E.g. + * a component is removed in a previous burst. + */ + return success; + } + + /** + * Processes a message burst received from the client. + * + * A burst can contain any number of RPC calls, including legacy variable + * change calls that are processed separately. + * + * Consecutive changes to the value of the same variable are combined and + * changeVariables() is only called once for them. This preserves the Vaadin + * 6 semantics for components and add-ons that do not use Vaadin 7 RPC + * directly. + * + * @param source + * @param root + * the root receiving the burst + * @param burst + * the content of the burst as a String to be parsed + * @return true if the processing of the burst was successful and there were + * no messages to non-existent components + */ + public boolean handleBurst(WrappedRequest source, Root root, + final String burst) { + boolean success = true; + try { + Set<Connector> enabledConnectors = new HashSet<Connector>(); + + List<MethodInvocation> invocations = parseInvocations( + root.getConnectorTracker(), burst); + for (MethodInvocation invocation : invocations) { + final ClientConnector connector = getConnector(root, + invocation.getConnectorId()); + + if (connector != null && connector.isConnectorEnabled()) { + enabledConnectors.add(connector); + } + } + + for (int i = 0; i < invocations.size(); i++) { + MethodInvocation invocation = invocations.get(i); + + final ClientConnector connector = getConnector(root, + invocation.getConnectorId()); + + if (connector == null) { + getLogger().log( + Level.WARNING, + "RPC call to " + invocation.getInterfaceName() + + "." + invocation.getMethodName() + + " received for connector " + + invocation.getConnectorId() + + " but no such connector could be found"); + continue; + } + + if (!enabledConnectors.contains(connector)) { + + if (invocation instanceof LegacyChangeVariablesInvocation) { + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + // TODO convert window close to a separate RPC call and + // handle above - not a variable change + + // Handle special case where window-close is called + // after the window has been removed from the + // application or the application has closed + Map<String, Object> changes = legacyInvocation + .getVariableChanges(); + if (changes.size() == 1 && changes.containsKey("close") + && Boolean.TRUE.equals(changes.get("close"))) { + // Silently ignore this + continue; + } + } + + // Connector is disabled, log a warning and move to the next + String msg = "Ignoring RPC call for disabled connector " + + connector.getClass().getName(); + if (connector instanceof Component) { + String caption = ((Component) connector).getCaption(); + if (caption != null) { + msg += ", caption=" + caption; + } + } + getLogger().warning(msg); + continue; + } + + if (invocation instanceof ServerRpcMethodInvocation) { + try { + ServerRpcManager.applyInvocation(connector, + (ServerRpcMethodInvocation) invocation); + } catch (RpcInvocationException e) { + Throwable realException = e.getCause(); + Component errorComponent = null; + if (connector instanceof Component) { + errorComponent = (Component) connector; + } + handleChangeVariablesError(root.getApplication(), + errorComponent, realException, null); + } + } else { + + // All code below is for legacy variable changes + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + Map<String, Object> changes = legacyInvocation + .getVariableChanges(); + try { + if (connector instanceof VariableOwner) { + changeVariables(source, (VariableOwner) connector, + changes); + } else { + throw new IllegalStateException( + "Received legacy variable change for " + + connector.getClass().getName() + + " (" + + connector.getConnectorId() + + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: " + + changes.keySet()); + } + } catch (Exception e) { + Component errorComponent = null; + if (connector instanceof Component) { + errorComponent = (Component) connector; + } else if (connector instanceof DragAndDropService) { + Object dropHandlerOwner = changes.get("dhowner"); + if (dropHandlerOwner instanceof Component) { + errorComponent = (Component) dropHandlerOwner; + } + } + handleChangeVariablesError(root.getApplication(), + errorComponent, e, changes); + } + } + } + } catch (JSONException e) { + getLogger().warning( + "Unable to parse RPC call from the client: " + + e.getMessage()); + // TODO or return success = false? + throw new RuntimeException(e); + } + + return success; + } + + /** + * Parse a message burst from the client into a list of MethodInvocation + * instances. + * + * @param connectorTracker + * The ConnectorTracker used to lookup connectors + * @param burst + * message string (JSON) + * @return list of MethodInvocation to perform + * @throws JSONException + */ + private List<MethodInvocation> parseInvocations( + ConnectorTracker connectorTracker, final String burst) + throws JSONException { + JSONArray invocationsJson = new JSONArray(burst); + + ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); + + MethodInvocation previousInvocation = null; + // parse JSON to MethodInvocations + for (int i = 0; i < invocationsJson.length(); ++i) { + + JSONArray invocationJson = invocationsJson.getJSONArray(i); + + MethodInvocation invocation = parseInvocation(invocationJson, + previousInvocation, connectorTracker); + if (invocation != null) { + // Can be null iff the invocation was a legacy invocation and it + // was merged with the previous one + invocations.add(invocation); + previousInvocation = invocation; + } + } + return invocations; + } + + private MethodInvocation parseInvocation(JSONArray invocationJson, + MethodInvocation previousInvocation, + ConnectorTracker connectorTracker) throws JSONException { + String connectorId = invocationJson.getString(0); + String interfaceName = invocationJson.getString(1); + String methodName = invocationJson.getString(2); + + JSONArray parametersJson = invocationJson.getJSONArray(3); + + if (LegacyChangeVariablesInvocation.isLegacyVariableChange( + interfaceName, methodName)) { + if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) { + previousInvocation = null; + } + + return parseLegacyChangeVariablesInvocation(connectorId, + interfaceName, methodName, + (LegacyChangeVariablesInvocation) previousInvocation, + parametersJson, connectorTracker); + } else { + return parseServerRpcInvocation(connectorId, interfaceName, + methodName, parametersJson, connectorTracker); + } + + } + + private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation( + String connectorId, String interfaceName, String methodName, + LegacyChangeVariablesInvocation previousInvocation, + JSONArray parametersJson, ConnectorTracker connectorTracker) + throws JSONException { + if (parametersJson.length() != 2) { + throw new JSONException( + "Invalid parameters in legacy change variables call. Expected 2, was " + + parametersJson.length()); + } + String variableName = parametersJson.getString(0); + UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType( + UidlValue.class, true, parametersJson.get(1), connectorTracker); + + Object value = uidlValue.getValue(); + + if (previousInvocation != null + && previousInvocation.getConnectorId().equals(connectorId)) { + previousInvocation.setVariableChange(variableName, value); + return null; + } else { + return new LegacyChangeVariablesInvocation(connectorId, + variableName, value); + } + } + + private ServerRpcMethodInvocation parseServerRpcInvocation( + String connectorId, String interfaceName, String methodName, + JSONArray parametersJson, ConnectorTracker connectorTracker) + throws JSONException { + ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( + connectorId, interfaceName, methodName, parametersJson.length()); + + Object[] parameters = new Object[parametersJson.length()]; + Type[] declaredRpcMethodParameterTypes = invocation.getMethod() + .getGenericParameterTypes(); + + for (int j = 0; j < parametersJson.length(); ++j) { + Object parameterValue = parametersJson.get(j); + Type parameterType = declaredRpcMethodParameterTypes[j]; + parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType, + parameterValue, connectorTracker); + } + invocation.setParameters(parameters); + return invocation; + } + + protected void changeVariables(Object source, final VariableOwner owner, + Map<String, Object> m) { + owner.changeVariables(source, m); + } + + protected ClientConnector getConnector(Root root, String connectorId) { + ClientConnector c = root.getConnectorTracker() + .getConnector(connectorId); + if (c == null + && connectorId.equals(getDragAndDropService().getConnectorId())) { + return getDragAndDropService(); + } + + return c; + } + + private DragAndDropService getDragAndDropService() { + if (dragAndDropService == null) { + dragAndDropService = new DragAndDropService(this); + } + return dragAndDropService; + } + + /** + * Reads the request data from the Request and returns it converted to an + * UTF-8 string. + * + * @param request + * @return + * @throws IOException + */ + protected String getRequestPayload(WrappedRequest request) + throws IOException { + + int requestLength = request.getContentLength(); + if (requestLength == 0) { + return null; + } + + ByteArrayOutputStream bout = requestLength <= 0 ? new ByteArrayOutputStream() + : new ByteArrayOutputStream(requestLength); + + InputStream inputStream = request.getInputStream(); + byte[] buffer = new byte[MAX_BUFFER_SIZE]; + + while (true) { + int read = inputStream.read(buffer); + if (read == -1) { + break; + } + bout.write(buffer, 0, read); + } + String result = new String(bout.toByteArray(), "utf-8"); + + return result; + } + + public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable { + private final Throwable throwable; + + public ErrorHandlerErrorEvent(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + } + + /** + * Handles an error (exception) that occurred when processing variable + * changes from the client or a failure of a file upload. + * + * For {@link AbstractField} components, + * {@link AbstractField#handleError(com.vaadin.ui.AbstractComponent.ComponentErrorEvent)} + * is called. In all other cases (or if the field does not handle the + * error), {@link ErrorListener#terminalError(ErrorEvent)} for the + * application error handler is called. + * + * @param application + * @param owner + * component that the error concerns + * @param e + * exception that occurred + * @param m + * map from variable names to values + */ + private void handleChangeVariablesError(Application application, + Component owner, Throwable t, Map<String, Object> m) { + boolean handled = false; + ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent( + owner, t, m); + + if (owner instanceof AbstractField) { + try { + handled = ((AbstractField<?>) owner).handleError(errorEvent); + } catch (Exception handlerException) { + /* + * If there is an error in the component error handler we pass + * the that error to the application error handler and continue + * processing the actual error + */ + application.getErrorHandler().terminalError( + new ErrorHandlerErrorEvent(handlerException)); + handled = false; + } + } + + if (!handled) { + application.getErrorHandler().terminalError(errorEvent); + } + + } + + /** + * Unescape encoded burst separator characters in a burst received from the + * client. This protects from separator injection attacks. + * + * @param encodedValue + * to decode + * @return decoded value + */ + protected String unescapeBurst(String encodedValue) { + final StringBuilder result = new StringBuilder(); + final StringCharacterIterator iterator = new StringCharacterIterator( + encodedValue); + char character = iterator.current(); + while (character != CharacterIterator.DONE) { + if (VAR_ESCAPE_CHARACTER == character) { + character = iterator.next(); + switch (character) { + case VAR_ESCAPE_CHARACTER + 0x30: + // escaped escape character + result.append(VAR_ESCAPE_CHARACTER); + break; + case VAR_BURST_SEPARATOR + 0x30: + // +0x30 makes these letters for easier reading + result.append((char) (character - 0x30)); + break; + case CharacterIterator.DONE: + // error + throw new RuntimeException( + "Communication error: Unexpected end of message"); + default: + // other escaped character - probably a client-server + // version mismatch + throw new RuntimeException( + "Invalid escaped character from the client - check that the widgetset and server versions match"); + } + } else { + // not a special character - add it to the result as is + result.append(character); + } + character = iterator.next(); + } + return result.toString(); + } + + /** + * Prints the queued (pending) locale definitions to a {@link PrintWriter} + * in a (UIDL) format that can be sent to the client and used there in + * formatting dates, times etc. + * + * @param outWriter + */ + private void printLocaleDeclarations(PrintWriter outWriter) { + /* + * ----------------------------- Sending Locale sensitive date + * ----------------------------- + */ + + // Send locale informations to client + outWriter.print(", \"locales\":["); + for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { + + final Locale l = generateLocale(locales.get(pendingLocalesIndex)); + // Locale name + outWriter.print("{\"name\":\"" + l.toString() + "\","); + + /* + * Month names (both short and full) + */ + final DateFormatSymbols dfs = new DateFormatSymbols(l); + final String[] short_months = dfs.getShortMonths(); + final String[] months = dfs.getMonths(); + outWriter.print("\"smn\":[\"" + + // ShortMonthNames + short_months[0] + "\",\"" + short_months[1] + "\",\"" + + short_months[2] + "\",\"" + short_months[3] + "\",\"" + + short_months[4] + "\",\"" + short_months[5] + "\",\"" + + short_months[6] + "\",\"" + short_months[7] + "\",\"" + + short_months[8] + "\",\"" + short_months[9] + "\",\"" + + short_months[10] + "\",\"" + short_months[11] + "\"" + + "],"); + outWriter.print("\"mn\":[\"" + + // MonthNames + months[0] + "\",\"" + months[1] + "\",\"" + months[2] + + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" + + months[5] + "\",\"" + months[6] + "\",\"" + months[7] + + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" + + months[10] + "\",\"" + months[11] + "\"" + "],"); + + /* + * Weekday names (both short and full) + */ + final String[] short_days = dfs.getShortWeekdays(); + final String[] days = dfs.getWeekdays(); + outWriter.print("\"sdn\":[\"" + + // ShortDayNames + short_days[1] + "\",\"" + short_days[2] + "\",\"" + + short_days[3] + "\",\"" + short_days[4] + "\",\"" + + short_days[5] + "\",\"" + short_days[6] + "\",\"" + + short_days[7] + "\"" + "],"); + outWriter.print("\"dn\":[\"" + + // DayNames + days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" + + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" + + days[7] + "\"" + "],"); + + /* + * First day of week (0 = sunday, 1 = monday) + */ + final Calendar cal = new GregorianCalendar(l); + outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); + + /* + * Date formatting (MM/DD/YYYY etc.) + */ + + DateFormat dateFormat = DateFormat.getDateTimeInstance( + DateFormat.SHORT, DateFormat.SHORT, l); + if (!(dateFormat instanceof SimpleDateFormat)) { + getLogger().warning( + "Unable to get default date pattern for locale " + + l.toString()); + dateFormat = new SimpleDateFormat(); + } + final String df = ((SimpleDateFormat) dateFormat).toPattern(); + + int timeStart = df.indexOf("H"); + if (timeStart < 0) { + timeStart = df.indexOf("h"); + } + final int ampm_first = df.indexOf("a"); + // E.g. in Korean locale AM/PM is before h:mm + // TODO should take that into consideration on client-side as well, + // now always h:mm a + if (ampm_first > 0 && ampm_first < timeStart) { + timeStart = ampm_first; + } + // Hebrew locale has time before the date + final boolean timeFirst = timeStart == 0; + String dateformat; + if (timeFirst) { + int dateStart = df.indexOf(' '); + if (ampm_first > dateStart) { + dateStart = df.indexOf(' ', ampm_first); + } + dateformat = df.substring(dateStart + 1); + } else { + dateformat = df.substring(0, timeStart - 1); + } + + outWriter.print("\"df\":\"" + dateformat.trim() + "\","); + + /* + * Time formatting (24 or 12 hour clock and AM/PM suffixes) + */ + final String timeformat = df.substring(timeStart, df.length()); + /* + * Doesn't return second or milliseconds. + * + * We use timeformat to determine 12/24-hour clock + */ + final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; + // TODO there are other possibilities as well, like 'h' in french + // (ignore them, too complicated) + final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." + : ":"; + // outWriter.print("\"tf\":\"" + timeformat + "\","); + outWriter.print("\"thc\":" + twelve_hour_clock + ","); + outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\""); + if (twelve_hour_clock) { + final String[] ampm = dfs.getAmPmStrings(); + outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] + + "\"]"); + } + outWriter.print("}"); + if (pendingLocalesIndex < locales.size() - 1) { + outWriter.print(","); + } + } + outWriter.print("]"); // Close locales + } + + /** + * Ends the Application. + * + * The browser is redirected to the Application logout URL set with + * {@link Application#setLogoutURL(String)}, or to the application URL if no + * logout URL is given. + * + * @param request + * the request instance. + * @param response + * the response to write to. + * @param application + * the Application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(WrappedRequest request, + WrappedResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + // clients JS app is still running, send a special json file to tell + // client that application has quit and where to point browser now + // Set the response type + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + openJsonMessage(outWriter, response); + outWriter.print("\"redirect\":{"); + outWriter.write("\"url\":\"" + logoutUrl + "\"}"); + closeJsonMessage(outWriter); + outWriter.flush(); + outWriter.close(); + out.flush(); + } + + protected void closeJsonMessage(PrintWriter outWriter) { + outWriter.print("}]"); + } + + /** + * Writes the opening of JSON message to be sent to client. + * + * @param outWriter + * @param response + */ + protected void openJsonMessage(PrintWriter outWriter, + WrappedResponse response) { + // Sets the response type + response.setContentType("application/json; charset=UTF-8"); + // some dirt to prevent cross site scripting + outWriter.print("for(;;);[{"); + } + + /** + * Returns dirty components which are in given window. Components in an + * invisible subtrees are omitted. + * + * @param w + * root window for which dirty components is to be fetched + * @return + */ + private ArrayList<ClientConnector> getDirtyVisibleConnectors( + ConnectorTracker connectorTracker) { + ArrayList<ClientConnector> dirtyConnectors = new ArrayList<ClientConnector>(); + for (ClientConnector c : connectorTracker.getDirtyConnectors()) { + if (isVisible(c)) { + dirtyConnectors.add(c); + } + } + + return dirtyConnectors; + } + + /** + * Queues a locale to be sent to the client (browser) for date and time + * entry etc. All locale specific information is derived from server-side + * {@link Locale} instances and sent to the client when needed, eliminating + * the need to use the {@link Locale} class and all the framework behind it + * on the client. + * + * @see Locale#toString() + * + * @param value + */ + public void requireLocale(String value) { + if (locales == null) { + locales = new ArrayList<String>(); + locales.add(application.getLocale().toString()); + pendingLocalesIndex = 0; + } + if (!locales.contains(value)) { + locales.add(value); + } + } + + /** + * Constructs a {@link Locale} instance to be sent to the client based on a + * short locale description string. + * + * @see #requireLocale(String) + * + * @param value + * @return + */ + private Locale generateLocale(String value) { + final String[] temp = value.split("_"); + if (temp.length == 1) { + return new Locale(temp[0]); + } else if (temp.length == 2) { + return new Locale(temp[0], temp[1]); + } else { + return new Locale(temp[0], temp[1], temp[2]); + } + } + + protected class InvalidUIDLSecurityKeyException extends + GeneralSecurityException { + + InvalidUIDLSecurityKeyException(String message) { + super(message); + } + + } + + private final HashMap<Class<? extends ClientConnector>, Integer> typeToKey = new HashMap<Class<? extends ClientConnector>, Integer>(); + private int nextTypeKey = 0; + + private BootstrapHandler bootstrapHandler; + + String getTagForType(Class<? extends ClientConnector> class1) { + Integer id = typeToKey.get(class1); + if (id == null) { + id = nextTypeKey++; + typeToKey.put(class1, id); + getLogger().log(Level.FINE, + "Mapping " + class1.getName() + " to " + id); + } + return id.toString(); + } + + /** + * Helper class for terminal to keep track of data that client is expected + * to know. + * + * TODO make customlayout templates (from theme) to be cached here. + */ + class ClientCache implements Serializable { + + private final Set<Object> res = new HashSet<Object>(); + + /** + * + * @param paintable + * @return true if the given class was added to cache + */ + boolean cache(Object object) { + return res.add(object); + } + + public void clear() { + res.clear(); + } + + } + + public String getStreamVariableTargetUrl(ClientConnector owner, + String name, StreamVariable value) { + /* + * We will use the same APP/* URI space as ApplicationResources but + * prefix url with UPLOAD + * + * eg. APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] + * + * SECKEY is created on each paint to make URL's unpredictable (to + * prevent CSRF attacks). + * + * NAME and PID from URI forms a key to fetch StreamVariable when + * handling post + */ + String paintableId = owner.getConnectorId(); + int rootId = owner.getRoot().getRootId(); + String key = rootId + "/" + paintableId + "/" + name; + + if (pidToNameToStreamVariable == null) { + pidToNameToStreamVariable = new HashMap<String, Map<String, StreamVariable>>(); + } + Map<String, StreamVariable> nameToStreamVariable = pidToNameToStreamVariable + .get(paintableId); + if (nameToStreamVariable == null) { + nameToStreamVariable = new HashMap<String, StreamVariable>(); + pidToNameToStreamVariable.put(paintableId, nameToStreamVariable); + } + nameToStreamVariable.put(name, value); + + if (streamVariableToSeckey == null) { + streamVariableToSeckey = new HashMap<StreamVariable, String>(); + } + String seckey = streamVariableToSeckey.get(value); + if (seckey == null) { + seckey = UUID.randomUUID().toString(); + streamVariableToSeckey.put(value, seckey); + } + + return ApplicationConnection.APP_PROTOCOL_PREFIX + + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey; + + } + + public void cleanStreamVariable(ClientConnector owner, String name) { + Map<String, StreamVariable> nameToStreamVar = pidToNameToStreamVariable + .get(owner.getConnectorId()); + nameToStreamVar.remove(name); + if (nameToStreamVar.isEmpty()) { + pidToNameToStreamVariable.remove(owner.getConnectorId()); + } + } + + /** + * Gets the bootstrap handler that should be used for generating the pages + * bootstrapping applications for this communication manager. + * + * @return the bootstrap handler to use + */ + private BootstrapHandler getBootstrapHandler() { + if (bootstrapHandler == null) { + bootstrapHandler = createBootstrapHandler(); + } + + return bootstrapHandler; + } + + protected abstract BootstrapHandler createBootstrapHandler(); + + protected boolean handleApplicationRequest(WrappedRequest request, + WrappedResponse response) throws IOException { + return application.handleRequest(request, response); + } + + public void handleBrowserDetailsRequest(WrappedRequest request, + WrappedResponse response, Application application) + throws IOException { + + // if we do not yet have a currentRoot, it should be initialized + // shortly, and we should send the initial UIDL + boolean sendUIDL = Root.getCurrent() == null; + + try { + CombinedRequest combinedRequest = new CombinedRequest(request); + + Root root = application.getRootForRequest(combinedRequest); + response.setContentType("application/json; charset=UTF-8"); + + // Use the same logic as for determined roots + BootstrapHandler bootstrapHandler = getBootstrapHandler(); + BootstrapContext context = bootstrapHandler.createContext( + combinedRequest, response, application, root.getRootId()); + + String widgetset = context.getWidgetsetName(); + String theme = context.getThemeName(); + String themeUri = bootstrapHandler.getThemeUri(context, theme); + + // TODO These are not required if it was only the init of the root + // that was delayed + JSONObject params = new JSONObject(); + params.put("widgetset", widgetset); + params.put("themeUri", themeUri); + // Root id might have changed based on e.g. window.name + params.put(ApplicationConnection.ROOT_ID_PARAMETER, + root.getRootId()); + if (sendUIDL) { + String initialUIDL = getInitialUIDL(combinedRequest, root); + params.put("uidl", initialUIDL); + } + + // NOTE! GateIn requires, for some weird reason, getOutputStream + // to be used instead of getWriter() (it seems to interpret + // application/json as a binary content type) + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + outWriter.write(params.toString()); + // NOTE GateIn requires the buffers to be flushed to work + outWriter.flush(); + out.flush(); + } catch (RootRequiresMoreInformationException e) { + // Requiring more information at this point is not allowed + // TODO handle in a better way + throw new RuntimeException(e); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Generates the initial UIDL message that can e.g. be included in a html + * page to avoid a separate round trip just for getting the UIDL. + * + * @param request + * the request that caused the initialization + * @param root + * the root for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws PaintException + * if an exception occurs while painting + * @throws JSONException + * if an exception occurs while encoding output + */ + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException { + // TODO maybe unify writeUidlResponse()? + StringWriter sWriter = new StringWriter(); + PrintWriter pWriter = new PrintWriter(sWriter); + pWriter.print("{"); + if (isXSRFEnabled(root.getApplication())) { + pWriter.print(getSecurityKeyUIDL(request)); + } + writeUidlResponse(request, true, pWriter, root, false); + pWriter.print("}"); + String initialUIDL = sWriter.toString(); + getLogger().log(Level.FINE, "Initial UIDL:" + initialUIDL); + return initialUIDL; + } + + /** + * Serve a connector resource from the classpath if the resource has + * previously been registered by calling + * {@link #registerResource(String, Class)}. Sending arbitrary files from + * the classpath is prevented by only accepting resource names that have + * explicitly been registered. Resources can currently only be registered by + * including a {@link JavaScript} or {@link StyleSheet} annotation on a + * Connector class. + * + * @param request + * @param response + * + * @throws IOException + */ + public void serveConnectorResource(WrappedRequest request, + WrappedResponse response) throws IOException { + + String pathInfo = request.getRequestPathInfo(); + // + 2 to also remove beginning and ending slashes + String resourceName = pathInfo + .substring(ApplicationConnection.CONNECTOR_RESOURCE_PREFIX + .length() + 2); + + final String mimetype = response.getDeploymentConfiguration() + .getMimeType(resourceName); + + // Security check: avoid accidentally serving from the root of the + // classpath instead of relative to the context class + if (resourceName.startsWith("/")) { + getLogger().warning( + "Connector resource request starting with / rejected: " + + resourceName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); + return; + } + + // Check that the resource name has been registered + Class<?> context; + synchronized (connectorResourceContexts) { + context = connectorResourceContexts.get(resourceName); + } + + // Security check: don't serve resource if the name hasn't been + // registered in the map + if (context == null) { + getLogger().warning( + "Connector resource request for unknown resource rejected: " + + resourceName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); + return; + } + + // Resolve file relative to the location of the context class + InputStream in = context.getResourceAsStream(resourceName); + if (in == null) { + getLogger().warning( + resourceName + " defined by " + context.getName() + + " not found. Verify that the file " + + context.getPackage().getName().replace('.', '/') + + '/' + resourceName + + " is available on the classpath."); + response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); + return; + } + + // TODO Check and set cache headers + + OutputStream out = null; + try { + if (mimetype != null) { + response.setContentType(mimetype); + } + + out = response.getOutputStream(); + + final byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE]; + + int bytesRead = 0; + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } finally { + try { + in.close(); + } catch (Exception e) { + // Do nothing + } + if (out != null) { + try { + out.close(); + } catch (Exception e) { + // Do nothing + } + } + } + } + + /** + * Handles file upload request submitted via Upload component. + * + * @param root + * The root for this request + * + * @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable) + * + * @param request + * @param response + * @throws IOException + * @throws InvalidUIDLSecurityKeyException + */ + public void handleFileUpload(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException, InvalidUIDLSecurityKeyException { + + /* + * URI pattern: APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] See + * #createReceiverUrl + */ + + String pathInfo = request.getRequestPathInfo(); + // strip away part until the data we are interested starts + int startOfData = pathInfo + .indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX) + + ServletPortletHelper.UPLOAD_URL_PREFIX.length(); + String uppUri = pathInfo.substring(startOfData); + String[] parts = uppUri.split("/", 4); // 0= rootid, 1 = cid, 2= name, 3 + // = sec key + String rootId = parts[0]; + String connectorId = parts[1]; + String variableName = parts[2]; + Root root = application.getRootById(Integer.parseInt(rootId)); + Root.setCurrent(root); + + StreamVariable streamVariable = getStreamVariable(connectorId, + variableName); + String secKey = streamVariableToSeckey.get(streamVariable); + if (secKey.equals(parts[3])) { + + ClientConnector source = getConnector(root, connectorId); + String contentType = request.getContentType(); + if (contentType.contains("boundary")) { + // Multipart requests contain boundary string + doHandleSimpleMultipartFileUpload(request, response, + streamVariable, variableName, source, + contentType.split("boundary=")[1]); + } else { + // if boundary string does not exist, the posted file is from + // XHR2.post(File) + doHandleXhrFilePost(request, response, streamVariable, + variableName, source, request.getContentLength()); + } + } else { + throw new InvalidUIDLSecurityKeyException( + "Security key in upload post did not match!"); + } + + } + + public StreamVariable getStreamVariable(String connectorId, + String variableName) { + Map<String, StreamVariable> map = pidToNameToStreamVariable + .get(connectorId); + if (map == null) { + return null; + } + StreamVariable streamVariable = map.get(variableName); + return streamVariable; + } + + /** + * Stream that extracts content from another stream until the boundary + * string is encountered. + * + * Public only for unit tests, should be considered private for all other + * purposes. + */ + public static class SimpleMultiPartInputStream extends InputStream { + + /** + * Counter of how many characters have been matched to boundary string + * from the stream + */ + int matchedCount = -1; + + /** + * Used as pointer when returning bytes after partly matched boundary + * string. + */ + int curBoundaryIndex = 0; + /** + * The byte found after a "promising start for boundary" + */ + private int bufferedByte = -1; + private boolean atTheEnd = false; + + private final char[] boundary; + + private final InputStream realInputStream; + + public SimpleMultiPartInputStream(InputStream realInputStream, + String boundaryString) { + boundary = (CRLF + DASHDASH + boundaryString).toCharArray(); + this.realInputStream = realInputStream; + } + + @Override + public int read() throws IOException { + if (atTheEnd) { + // End boundary reached, nothing more to read + return -1; + } else if (bufferedByte >= 0) { + /* Purge partially matched boundary if there was such */ + return getBuffered(); + } else if (matchedCount != -1) { + /* + * Special case where last "failed" matching ended with first + * character from boundary string + */ + return matchForBoundary(); + } else { + int fromActualStream = realInputStream.read(); + if (fromActualStream == -1) { + // unexpected end of stream + throw new IOException( + "The multipart stream ended unexpectedly"); + } + if (boundary[0] == fromActualStream) { + /* + * If matches the first character in boundary string, start + * checking if the boundary is fetched. + */ + return matchForBoundary(); + } + return fromActualStream; + } + } + + /** + * Reads the input to expect a boundary string. Expects that the first + * character has already been matched. + * + * @return -1 if the boundary was matched, else returns the first byte + * from boundary + * @throws IOException + */ + private int matchForBoundary() throws IOException { + matchedCount = 0; + /* + * Going to "buffered mode". Read until full boundary match or a + * different character. + */ + while (true) { + matchedCount++; + if (matchedCount == boundary.length) { + /* + * The whole boundary matched so we have reached the end of + * file + */ + atTheEnd = true; + return -1; + } + int fromActualStream = realInputStream.read(); + if (fromActualStream != boundary[matchedCount]) { + /* + * Did not find full boundary, cache the mismatching byte + * and start returning the partially matched boundary. + */ + bufferedByte = fromActualStream; + return getBuffered(); + } + } + } + + /** + * Returns the partly matched boundary string and the byte following + * that. + * + * @return + * @throws IOException + */ + private int getBuffered() throws IOException { + int b; + if (matchedCount == 0) { + // The boundary has been returned, return the buffered byte. + b = bufferedByte; + bufferedByte = -1; + matchedCount = -1; + } else { + b = boundary[curBoundaryIndex++]; + if (curBoundaryIndex == matchedCount) { + // The full boundary has been returned, remaining is the + // char that did not match the boundary. + + curBoundaryIndex = 0; + if (bufferedByte != boundary[0]) { + /* + * next call for getBuffered will return the + * bufferedByte that came after the partial boundary + * match + */ + matchedCount = 0; + } else { + /* + * Special case where buffered byte again matches the + * boundaryString. This could be the start of the real + * end boundary. + */ + matchedCount = 0; + bufferedByte = -1; + } + } + } + if (b == -1) { + throw new IOException("The multipart stream ended unexpectedly"); + } + return b; + } + } + + private static final Logger getLogger() { + return Logger.getLogger(AbstractCommunicationManager.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java new file mode 100644 index 0000000000..7b51712904 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java @@ -0,0 +1,143 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Constructor; +import java.util.Iterator; +import java.util.Properties; +import java.util.ServiceLoader; + +import com.vaadin.terminal.DeploymentConfiguration; + +public abstract class AbstractDeploymentConfiguration implements + DeploymentConfiguration { + + private final Class<?> systemPropertyBaseClass; + private final Properties applicationProperties = new Properties(); + private AddonContext addonContext; + + public AbstractDeploymentConfiguration(Class<?> systemPropertyBaseClass) { + this.systemPropertyBaseClass = systemPropertyBaseClass; + } + + @Override + public String getApplicationOrSystemProperty(String propertyName, + String defaultValue) { + + String val = null; + + // Try application properties + val = getApplicationProperty(propertyName); + if (val != null) { + return val; + } + + // Try system properties + val = getSystemProperty(propertyName); + if (val != null) { + return val; + } + + return defaultValue; + } + + /** + * Gets an system property value. + * + * @param parameterName + * the Name or the parameter. + * @return String value or null if not found + */ + protected String getSystemProperty(String parameterName) { + String val = null; + + String pkgName; + final Package pkg = systemPropertyBaseClass.getPackage(); + if (pkg != null) { + pkgName = pkg.getName(); + } else { + final String className = systemPropertyBaseClass.getName(); + pkgName = new String(className.toCharArray(), 0, + className.lastIndexOf('.')); + } + val = System.getProperty(pkgName + "." + parameterName); + if (val != null) { + return val; + } + + // Try lowercased system properties + val = System.getProperty(pkgName + "." + parameterName.toLowerCase()); + return val; + } + + @Override + public ClassLoader getClassLoader() { + final String classLoaderName = getApplicationOrSystemProperty( + "ClassLoader", null); + ClassLoader classLoader; + if (classLoaderName == null) { + classLoader = getClass().getClassLoader(); + } else { + try { + final Class<?> classLoaderClass = getClass().getClassLoader() + .loadClass(classLoaderName); + final Constructor<?> c = classLoaderClass + .getConstructor(new Class[] { ClassLoader.class }); + classLoader = (ClassLoader) c + .newInstance(new Object[] { getClass().getClassLoader() }); + } catch (final Exception e) { + throw new RuntimeException( + "Could not find specified class loader: " + + classLoaderName, e); + } + } + return classLoader; + } + + /** + * Gets an application property value. + * + * @param parameterName + * the Name or the parameter. + * @return String value or null if not found + */ + protected String getApplicationProperty(String parameterName) { + + String val = applicationProperties.getProperty(parameterName); + if (val != null) { + return val; + } + + // Try lower case application properties for backward compatibility with + // 3.0.2 and earlier + val = applicationProperties.getProperty(parameterName.toLowerCase()); + + return val; + } + + @Override + public Properties getInitParameters() { + return applicationProperties; + } + + @Override + public Iterator<AddonContextListener> getAddonContextListeners() { + // Called once for init and then no more, so there's no point in caching + // the instance + ServiceLoader<AddonContextListener> contextListenerLoader = ServiceLoader + .load(AddonContextListener.class, getClassLoader()); + return contextListenerLoader.iterator(); + } + + @Override + public void setAddonContext(AddonContext addonContext) { + this.addonContext = addonContext; + } + + @Override + public AddonContext getAddonContext() { + return addonContext; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java b/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java new file mode 100644 index 0000000000..d3474e736e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingEvent; + +/** + * Abstract base class for StreamingEvent implementations. + */ +@SuppressWarnings("serial") +abstract class AbstractStreamingEvent implements StreamingEvent { + private final String type; + private final String filename; + private final long contentLength; + private final long bytesReceived; + + @Override + public final String getFileName() { + return filename; + } + + @Override + public final String getMimeType() { + return type; + } + + protected AbstractStreamingEvent(String filename, String type, long length, + long bytesReceived) { + this.filename = filename; + this.type = type; + contentLength = length; + this.bytesReceived = bytesReceived; + } + + @Override + public final long getContentLength() { + return contentLength; + } + + @Override + public final long getBytesReceived() { + return bytesReceived; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java new file mode 100644 index 0000000000..3a33621d10 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java @@ -0,0 +1,268 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * Base class for web application contexts (including portlet contexts) that + * handles the common tasks. + */ +public abstract class AbstractWebApplicationContext implements + ApplicationContext, HttpSessionBindingListener, Serializable { + + protected Collection<TransactionListener> listeners = Collections + .synchronizedList(new LinkedList<TransactionListener>()); + + protected final HashSet<Application> applications = new HashSet<Application>(); + + protected WebBrowser browser = new WebBrowser(); + + protected HashMap<Application, AbstractCommunicationManager> applicationToAjaxAppMgrMap = new HashMap<Application, AbstractCommunicationManager>(); + + private long totalSessionTime = 0; + + private long lastRequestTime = -1; + + @Override + public void addTransactionListener(TransactionListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + @Override + public void removeTransactionListener(TransactionListener listener) { + listeners.remove(listener); + } + + /** + * Sends a notification that a transaction is starting. + * + * @param application + * The application associated with the transaction. + * @param request + * the HTTP or portlet request that triggered the transaction. + */ + protected void startTransaction(Application application, Object request) { + ArrayList<TransactionListener> currentListeners; + synchronized (listeners) { + currentListeners = new ArrayList<TransactionListener>(listeners); + } + for (TransactionListener listener : currentListeners) { + listener.transactionStart(application, request); + } + } + + /** + * Sends a notification that a transaction has ended. + * + * @param application + * The application associated with the transaction. + * @param request + * the HTTP or portlet request that triggered the transaction. + */ + protected void endTransaction(Application application, Object request) { + LinkedList<Exception> exceptions = null; + + ArrayList<TransactionListener> currentListeners; + synchronized (listeners) { + currentListeners = new ArrayList<TransactionListener>(listeners); + } + + for (TransactionListener listener : currentListeners) { + try { + listener.transactionEnd(application, request); + } catch (final RuntimeException t) { + if (exceptions == null) { + exceptions = new LinkedList<Exception>(); + } + exceptions.add(t); + } + } + + // If any runtime exceptions occurred, throw a combined exception + if (exceptions != null) { + final StringBuffer msg = new StringBuffer(); + for (Exception e : exceptions) { + if (msg.length() == 0) { + msg.append("\n\n--------------------------\n\n"); + } + msg.append(e.getMessage() + "\n"); + final StringWriter trace = new StringWriter(); + e.printStackTrace(new PrintWriter(trace, true)); + msg.append(trace.toString()); + } + throw new RuntimeException(msg.toString()); + } + } + + /** + * @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent) + */ + @Override + public void valueBound(HttpSessionBindingEvent arg0) { + // We are not interested in bindings + } + + /** + * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent) + */ + @Override + public void valueUnbound(HttpSessionBindingEvent event) { + // If we are going to be unbound from the session, the session must be + // closing + try { + while (!applications.isEmpty()) { + final Application app = applications.iterator().next(); + app.close(); + removeApplication(app); + } + } catch (Exception e) { + // This should never happen but is possible with rare + // configurations (e.g. robustness tests). If you have one + // thread doing HTTP socket write and another thread trying to + // remove same application here. Possible if you got e.g. session + // lifetime 1 min but socket write may take longer than 1 min. + // FIXME: Handle exception + getLogger().log(Level.SEVERE, + "Could not remove application, leaking memory.", e); + } + } + + /** + * Get the web browser associated with this application context. + * + * Because application context is related to the http session and server + * maintains one session per browser-instance, each context has exactly one + * web browser associated with it. + * + * @return + */ + public WebBrowser getBrowser() { + return browser; + } + + @Override + public Collection<Application> getApplications() { + return Collections.unmodifiableCollection(applications); + } + + protected void removeApplication(Application application) { + applications.remove(application); + applicationToAjaxAppMgrMap.remove(application); + } + + @Override + public String generateApplicationResourceURL(ApplicationResource resource, + String mapKey) { + + final String filename = resource.getFilename(); + if (filename == null) { + return ApplicationConnection.APP_PROTOCOL_PREFIX + + ApplicationConnection.APP_REQUEST_PATH + mapKey + "/"; + } else { + // #7738 At least Tomcat and JBoss refuses requests containing + // encoded slashes or backslashes in URLs. Application resource URLs + // should really be passed in another way than as part of the path + // in the future. + String encodedFileName = urlEncode(filename).replace("%2F", "/") + .replace("%5C", "\\"); + return ApplicationConnection.APP_PROTOCOL_PREFIX + + ApplicationConnection.APP_REQUEST_PATH + mapKey + "/" + + encodedFileName; + } + + } + + static String urlEncode(String filename) { + try { + return URLEncoder.encode(filename, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException( + "UTF-8 charset not available (\"this should never happen\")", + e); + } + } + + @Override + public boolean isApplicationResourceURL(URL context, String relativeUri) { + // If the relative uri is null, we are ready + if (relativeUri == null) { + return false; + } + + // Resolves the prefix + String prefix = relativeUri; + final int index = relativeUri.indexOf('/'); + if (index >= 0) { + prefix = relativeUri.substring(0, index); + } + + // Handles the resource requests + return (prefix.equals("APP")); + } + + @Override + public String getURLKey(URL context, String relativeUri) { + final int index = relativeUri.indexOf('/'); + final int next = relativeUri.indexOf('/', index + 1); + if (next < 0) { + return null; + } + return relativeUri.substring(index + 1, next); + } + + /** + * @return The total time spent servicing requests in this session. + */ + public long getTotalSessionTime() { + return totalSessionTime; + } + + /** + * Sets the time spent servicing the last request in the session and updates + * the total time spent servicing requests in this session. + * + * @param time + * the time spent in the last request. + */ + public void setLastRequestTime(long time) { + lastRequestTime = time; + totalSessionTime += time; + } + + /** + * @return the time spent servicing the last request in this session. + */ + public long getLastRequestTime() { + return lastRequestTime; + } + + private Logger getLogger() { + return Logger.getLogger(AbstractWebApplicationContext.class.getName()); + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContext.java b/server/src/com/vaadin/terminal/gwt/server/AddonContext.java new file mode 100644 index 0000000000..41e9046e22 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AddonContext.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.vaadin.Application; +import com.vaadin.event.EventRouter; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.tools.ReflectTools; + +public class AddonContext { + private static final Method APPLICATION_STARTED_METHOD = ReflectTools + .findMethod(ApplicationStartedListener.class, "applicationStarted", + ApplicationStartedEvent.class); + + private final DeploymentConfiguration deploymentConfiguration; + + private final EventRouter eventRouter = new EventRouter(); + + private List<BootstrapListener> bootstrapListeners = new ArrayList<BootstrapListener>(); + + private List<AddonContextListener> initedListeners = new ArrayList<AddonContextListener>(); + + public AddonContext(DeploymentConfiguration deploymentConfiguration) { + this.deploymentConfiguration = deploymentConfiguration; + deploymentConfiguration.setAddonContext(this); + } + + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + public void init() { + AddonContextEvent event = new AddonContextEvent(this); + Iterator<AddonContextListener> listeners = deploymentConfiguration + .getAddonContextListeners(); + while (listeners.hasNext()) { + AddonContextListener listener = listeners.next(); + listener.contextCreated(event); + initedListeners.add(listener); + } + } + + public void destroy() { + AddonContextEvent event = new AddonContextEvent(this); + for (AddonContextListener listener : initedListeners) { + listener.contextDestoryed(event); + } + } + + public void addBootstrapListener(BootstrapListener listener) { + bootstrapListeners.add(listener); + } + + public void applicationStarted(Application application) { + eventRouter.fireEvent(new ApplicationStartedEvent(this, application)); + for (BootstrapListener l : bootstrapListeners) { + application.addBootstrapListener(l); + } + } + + public void addApplicationStartedListener( + ApplicationStartedListener applicationStartListener) { + eventRouter.addListener(ApplicationStartedEvent.class, + applicationStartListener, APPLICATION_STARTED_METHOD); + } + + public void removeApplicationStartedListener( + ApplicationStartedListener applicationStartListener) { + eventRouter.removeListener(ApplicationStartedEvent.class, + applicationStartListener, APPLICATION_STARTED_METHOD); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java b/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java new file mode 100644 index 0000000000..33f681499f --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java @@ -0,0 +1,19 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventObject; + +public class AddonContextEvent extends EventObject { + + public AddonContextEvent(AddonContext source) { + super(source); + } + + public AddonContext getAddonContext() { + return (AddonContext) getSource(); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java b/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java new file mode 100644 index 0000000000..93e7df4ede --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java @@ -0,0 +1,13 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventListener; + +public interface AddonContextListener extends EventListener { + public void contextCreated(AddonContextEvent event); + + public void contextDestoryed(AddonContextEvent event); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java new file mode 100644 index 0000000000..788c48267e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.portlet.PortletConfig; +import javax.portlet.PortletException; + +import com.vaadin.Application; +import com.vaadin.terminal.gwt.server.ServletPortletHelper.ApplicationClassException; + +/** + * TODO Write documentation, fix JavaDoc tags. + * + * @author peholmst + */ +public class ApplicationPortlet2 extends AbstractApplicationPortlet { + + private Class<? extends Application> applicationClass; + + @Override + public void init(PortletConfig config) throws PortletException { + super.init(config); + try { + applicationClass = ServletPortletHelper + .getApplicationClass(getDeploymentConfiguration()); + } catch (ApplicationClassException e) { + throw new PortletException(e); + } + } + + @Override + protected Class<? extends Application> getApplicationClass() { + return applicationClass; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java new file mode 100644 index 0000000000..42726c933e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java @@ -0,0 +1,55 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; + +public class ApplicationResourceHandler implements RequestHandler { + private static final Pattern APP_RESOURCE_PATTERN = Pattern + .compile("^/?APP/(\\d+)/.*"); + + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + // Check for application resources + String requestPath = request.getRequestPathInfo(); + if (requestPath == null) { + return false; + } + Matcher resourceMatcher = APP_RESOURCE_PATTERN.matcher(requestPath); + + if (resourceMatcher.matches()) { + ApplicationResource resource = application + .getResource(resourceMatcher.group(1)); + if (resource != null) { + DownloadStream stream = resource.getStream(); + if (stream != null) { + stream.setCacheTime(resource.getCacheTime()); + stream.writeTo(response); + return true; + } + } + // We get here if the url looks like an application resource but no + // resource can be served + response.sendError(HttpServletResponse.SC_NOT_FOUND, + request.getRequestPathInfo() + " can not be found"); + return true; + } + + return false; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java new file mode 100644 index 0000000000..1af49e0da0 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java @@ -0,0 +1,78 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import com.vaadin.Application; +import com.vaadin.terminal.gwt.server.ServletPortletHelper.ApplicationClassException; + +/** + * This servlet connects a Vaadin Application to Web. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + +@SuppressWarnings("serial") +public class ApplicationServlet extends AbstractApplicationServlet { + + // Private fields + private Class<? extends Application> applicationClass; + + /** + * Called by the servlet container to indicate to a servlet that the servlet + * is being placed into service. + * + * @param servletConfig + * the object containing the servlet's configuration and + * initialization parameters + * @throws javax.servlet.ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + @Override + public void init(javax.servlet.ServletConfig servletConfig) + throws javax.servlet.ServletException { + super.init(servletConfig); + + // Loads the application class using the classloader defined in the + // deployment configuration + + try { + applicationClass = ServletPortletHelper + .getApplicationClass(getDeploymentConfiguration()); + } catch (ApplicationClassException e) { + throw new ServletException(e); + } + } + + @Override + protected Application getNewApplication(HttpServletRequest request) + throws ServletException { + + // Creates a new application instance + try { + final Application application = getApplicationClass().newInstance(); + + return application; + } catch (final IllegalAccessException e) { + throw new ServletException("getNewApplication failed", e); + } catch (final InstantiationException e) { + throw new ServletException("getNewApplication failed", e); + } catch (ClassNotFoundException e) { + throw new ServletException("getNewApplication failed", e); + } + } + + @Override + protected Class<? extends Application> getApplicationClass() + throws ClassNotFoundException { + return applicationClass; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java new file mode 100644 index 0000000000..339b88222e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventObject; + +import com.vaadin.Application; + +public class ApplicationStartedEvent extends EventObject { + private final Application application; + + public ApplicationStartedEvent(AddonContext context, + Application application) { + super(context); + this.application = application; + } + + public AddonContext getContext() { + return (AddonContext) getSource(); + } + + public Application getApplication() { + return application; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java new file mode 100644 index 0000000000..87884a0fda --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java @@ -0,0 +1,11 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventListener; + +public interface ApplicationStartedListener extends EventListener { + public void applicationStarted(ApplicationStartedEvent event); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java new file mode 100644 index 0000000000..4731a5b79f --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +public class BootstrapDom { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java new file mode 100644 index 0000000000..bcf098b5aa --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.List; + +import org.jsoup.nodes.Node; + +import com.vaadin.Application; +import com.vaadin.terminal.WrappedRequest; + +public class BootstrapFragmentResponse extends BootstrapResponse { + private final List<Node> fragmentNodes; + + public BootstrapFragmentResponse(BootstrapHandler handler, + WrappedRequest request, List<Node> fragmentNodes, + Application application, Integer rootId) { + super(handler, request, application, rootId); + this.fragmentNodes = fragmentNodes; + } + + public List<Node> getFragmentNodes() { + return fragmentNodes; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java new file mode 100644 index 0000000000..e89737337b --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java @@ -0,0 +1,570 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import org.jsoup.nodes.DataNode; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.DocumentType; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.parser.Tag; + +import com.vaadin.Application; +import com.vaadin.RootRequiresMoreInformationException; +import com.vaadin.Version; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.Root; + +public abstract class BootstrapHandler implements RequestHandler { + + protected class BootstrapContext implements Serializable { + + private final WrappedResponse response; + private final BootstrapFragmentResponse bootstrapResponse; + + private String widgetsetName; + private String themeName; + private String appId; + + public BootstrapContext(WrappedResponse response, + BootstrapFragmentResponse bootstrapResponse) { + this.response = response; + this.bootstrapResponse = bootstrapResponse; + } + + public WrappedResponse getResponse() { + return response; + } + + public WrappedRequest getRequest() { + return bootstrapResponse.getRequest(); + } + + public Application getApplication() { + return bootstrapResponse.getApplication(); + } + + public Integer getRootId() { + return bootstrapResponse.getRootId(); + } + + public Root getRoot() { + return bootstrapResponse.getRoot(); + } + + public String getWidgetsetName() { + if (widgetsetName == null) { + Root root = getRoot(); + if (root != null) { + widgetsetName = getWidgetsetForRoot(this); + } + } + return widgetsetName; + } + + public String getThemeName() { + if (themeName == null) { + Root root = getRoot(); + if (root != null) { + themeName = findAndEscapeThemeName(this); + } + } + return themeName; + } + + public String getAppId() { + if (appId == null) { + appId = getApplicationId(this); + } + return appId; + } + + public BootstrapFragmentResponse getBootstrapResponse() { + return bootstrapResponse; + } + + } + + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + + // TODO Should all urls be handled here? + Integer rootId = null; + try { + Root root = application.getRootForRequest(request); + if (root == null) { + writeError(response, new Throwable("No Root found")); + return true; + } + + rootId = Integer.valueOf(root.getRootId()); + } catch (RootRequiresMoreInformationException e) { + // Just keep going without rootId + } + + try { + BootstrapContext context = createContext(request, response, + application, rootId); + setupMainDiv(context); + + BootstrapFragmentResponse fragmentResponse = context + .getBootstrapResponse(); + application.modifyBootstrapResponse(fragmentResponse); + + String html = getBootstrapHtml(context); + + writeBootstrapPage(response, html); + } catch (JSONException e) { + writeError(response, e); + } + + return true; + } + + private String getBootstrapHtml(BootstrapContext context) { + WrappedRequest request = context.getRequest(); + WrappedResponse response = context.getResponse(); + DeploymentConfiguration deploymentConfiguration = request + .getDeploymentConfiguration(); + + BootstrapFragmentResponse fragmentResponse = context + .getBootstrapResponse(); + + if (deploymentConfiguration.isStandalone(request)) { + Map<String, Object> headers = new LinkedHashMap<String, Object>(); + Document document = Document.createShell(""); + BootstrapPageResponse pageResponse = new BootstrapPageResponse( + this, request, document, headers, context.getApplication(), + context.getRootId()); + List<Node> fragmentNodes = fragmentResponse.getFragmentNodes(); + Element body = document.body(); + for (Node node : fragmentNodes) { + body.appendChild(node); + } + + setupStandaloneDocument(context, pageResponse); + context.getApplication().modifyBootstrapResponse(pageResponse); + + sendBootstrapHeaders(response, headers); + + return document.outerHtml(); + } else { + StringBuilder sb = new StringBuilder(); + for (Node node : fragmentResponse.getFragmentNodes()) { + if (sb.length() != 0) { + sb.append('\n'); + } + sb.append(node.outerHtml()); + } + + return sb.toString(); + } + } + + private void sendBootstrapHeaders(WrappedResponse response, + Map<String, Object> headers) { + Set<Entry<String, Object>> entrySet = headers.entrySet(); + for (Entry<String, Object> header : entrySet) { + Object value = header.getValue(); + if (value instanceof String) { + response.setHeader(header.getKey(), (String) value); + } else if (value instanceof Long) { + response.setDateHeader(header.getKey(), + ((Long) value).longValue()); + } else { + throw new RuntimeException("Unsupported header value: " + value); + } + } + } + + private void writeBootstrapPage(WrappedResponse response, String html) + throws IOException { + response.setContentType("text/html"); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + response.getOutputStream(), "UTF-8")); + writer.append(html); + writer.close(); + } + + private void setupStandaloneDocument(BootstrapContext context, + BootstrapPageResponse response) { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + + Document document = response.getDocument(); + + DocumentType doctype = new DocumentType("html", + "-//W3C//DTD XHTML 1.0 Transitional//EN", + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd", + document.baseUri()); + document.child(0).before(doctype); + document.body().parent().attr("xmlns", "http://www.w3.org/1999/xhtml"); + + Element head = document.head(); + head.appendElement("meta").attr("http-equiv", "Content-Type") + .attr("content", "text/html; charset=utf-8"); + + // Chrome frame in all versions of IE (only if Chrome frame is + // installed) + head.appendElement("meta").attr("http-equiv", "X-UA-Compatible") + .attr("content", "chrome=1"); + + Root root = context.getRoot(); + String title = ((root == null || root.getCaption() == null) ? "" : root + .getCaption()); + head.appendElement("title").appendText(title); + + head.appendElement("style").attr("type", "text/css") + .appendText("html, body {height:100%;margin:0;}"); + + // Add favicon links + String themeName = context.getThemeName(); + if (themeName != null) { + String themeUri = getThemeUri(context, themeName); + head.appendElement("link").attr("rel", "shortcut icon") + .attr("type", "image/vnd.microsoft.icon") + .attr("href", themeUri + "/favicon.ico"); + head.appendElement("link").attr("rel", "icon") + .attr("type", "image/vnd.microsoft.icon") + .attr("href", themeUri + "/favicon.ico"); + } + + Element body = document.body(); + body.attr("scroll", "auto"); + body.addClass(ApplicationConnection.GENERATED_BODY_CLASSNAME); + } + + public BootstrapContext createContext(WrappedRequest request, + WrappedResponse response, Application application, Integer rootId) { + BootstrapContext context = new BootstrapContext(response, + new BootstrapFragmentResponse(this, request, + new ArrayList<Node>(), application, rootId)); + return context; + } + + protected String getMainDivStyle(BootstrapContext context) { + return null; + } + + /** + * Creates and returns a unique ID for the DIV where the application is to + * be rendered. + * + * @param context + * + * @return the id to use in the DOM + */ + protected abstract String getApplicationId(BootstrapContext context); + + public String getWidgetsetForRoot(BootstrapContext context) { + Root root = context.getRoot(); + WrappedRequest request = context.getRequest(); + + String widgetset = root.getApplication().getWidgetsetForRoot(root); + if (widgetset == null) { + widgetset = request.getDeploymentConfiguration() + .getConfiguredWidgetset(request); + } + + widgetset = AbstractApplicationServlet.stripSpecialChars(widgetset); + return widgetset; + } + + /** + * Method to write the div element into which that actual Vaadin application + * is rendered. + * <p> + * Override this method if you want to add some custom html around around + * the div element into which the actual Vaadin application will be + * rendered. + * + * @param context + * + * @throws IOException + * @throws JSONException + */ + private void setupMainDiv(BootstrapContext context) throws IOException, + JSONException { + String style = getMainDivStyle(context); + + /*- Add classnames; + * .v-app + * .v-app-loading + * .v-app-<simpleName for app class> + *- Additionally added from javascript: + * .v-theme-<themeName, remove non-alphanum> + */ + + String appClass = "v-app-" + + context.getApplication().getClass().getSimpleName(); + + String classNames = "v-app " + appClass; + List<Node> fragmentNodes = context.getBootstrapResponse() + .getFragmentNodes(); + + Element mainDiv = new Element(Tag.valueOf("div"), ""); + mainDiv.attr("id", context.getAppId()); + mainDiv.addClass(classNames); + if (style != null && style.length() != 0) { + mainDiv.attr("style", style); + } + mainDiv.appendElement("div").addClass("v-app-loading"); + mainDiv.appendElement("noscript") + .append("You have to enable javascript in your browser to use an application built with Vaadin."); + fragmentNodes.add(mainDiv); + + WrappedRequest request = context.getRequest(); + + DeploymentConfiguration deploymentConfiguration = request + .getDeploymentConfiguration(); + String staticFileLocation = deploymentConfiguration + .getStaticFileLocation(request); + + fragmentNodes + .add(new Element(Tag.valueOf("iframe"), "") + .attr("tabIndex", "-1") + .attr("id", "__gwt_historyFrame") + .attr("style", + "position:absolute;width:0;height:0;border:0;overflow:hidden") + .attr("src", "javascript:false")); + + String bootstrapLocation = staticFileLocation + + "/VAADIN/vaadinBootstrap.js"; + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type", + "text/javascript").attr("src", bootstrapLocation)); + Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript"); + + StringBuilder builder = new StringBuilder(); + builder.append("//<![CDATA[\n"); + builder.append("if (!window.vaadin) alert(" + + JSONObject.quote("Failed to load the bootstrap javascript: " + + bootstrapLocation) + ");\n"); + + appendMainScriptTagContents(context, builder); + + builder.append("//]]>"); + mainScriptTag.appendChild(new DataNode(builder.toString(), + mainScriptTag.baseUri())); + fragmentNodes.add(mainScriptTag); + + } + + protected void appendMainScriptTagContents(BootstrapContext context, + StringBuilder builder) throws JSONException, IOException { + JSONObject defaults = getDefaultParameters(context); + JSONObject appConfig = getApplicationParameters(context); + + boolean isDebug = !context.getApplication().isProductionMode(); + + builder.append("vaadin.setDefaults("); + appendJsonObject(builder, defaults, isDebug); + builder.append(");\n"); + + builder.append("vaadin.initApplication(\""); + builder.append(context.getAppId()); + builder.append("\","); + appendJsonObject(builder, appConfig, isDebug); + builder.append(");\n"); + } + + private static void appendJsonObject(StringBuilder builder, + JSONObject jsonObject, boolean isDebug) throws JSONException { + if (isDebug) { + builder.append(jsonObject.toString(4)); + } else { + builder.append(jsonObject.toString()); + } + } + + protected JSONObject getApplicationParameters(BootstrapContext context) + throws JSONException, PaintException { + Application application = context.getApplication(); + Integer rootId = context.getRootId(); + + JSONObject appConfig = new JSONObject(); + + if (rootId != null) { + appConfig.put(ApplicationConnection.ROOT_ID_PARAMETER, rootId); + } + + if (context.getThemeName() != null) { + appConfig.put("themeUri", + getThemeUri(context, context.getThemeName())); + } + + JSONObject versionInfo = new JSONObject(); + versionInfo.put("vaadinVersion", Version.getFullVersion()); + versionInfo.put("applicationVersion", application.getVersion()); + appConfig.put("versionInfo", versionInfo); + + appConfig.put("widgetset", context.getWidgetsetName()); + + if (rootId == null || application.isRootInitPending(rootId.intValue())) { + appConfig.put("initialPath", context.getRequest() + .getRequestPathInfo()); + + Map<String, String[]> parameterMap = context.getRequest() + .getParameterMap(); + appConfig.put("initialParams", parameterMap); + } else { + // write the initial UIDL into the config + appConfig.put("uidl", + getInitialUIDL(context.getRequest(), context.getRoot())); + } + + return appConfig; + } + + protected JSONObject getDefaultParameters(BootstrapContext context) + throws JSONException { + JSONObject defaults = new JSONObject(); + + WrappedRequest request = context.getRequest(); + Application application = context.getApplication(); + + // Get system messages + Application.SystemMessages systemMessages = AbstractApplicationServlet + .getSystemMessages(application.getClass()); + if (systemMessages != null) { + // Write the CommunicationError -message to client + JSONObject comErrMsg = new JSONObject(); + comErrMsg.put("caption", + systemMessages.getCommunicationErrorCaption()); + comErrMsg.put("message", + systemMessages.getCommunicationErrorMessage()); + comErrMsg.put("url", systemMessages.getCommunicationErrorURL()); + + defaults.put("comErrMsg", comErrMsg); + + JSONObject authErrMsg = new JSONObject(); + authErrMsg.put("caption", + systemMessages.getAuthenticationErrorCaption()); + authErrMsg.put("message", + systemMessages.getAuthenticationErrorMessage()); + authErrMsg.put("url", systemMessages.getAuthenticationErrorURL()); + + defaults.put("authErrMsg", authErrMsg); + } + + DeploymentConfiguration deploymentConfiguration = request + .getDeploymentConfiguration(); + String staticFileLocation = deploymentConfiguration + .getStaticFileLocation(request); + String widgetsetBase = staticFileLocation + "/" + + AbstractApplicationServlet.WIDGETSET_DIRECTORY_PATH; + defaults.put("widgetsetBase", widgetsetBase); + + if (!application.isProductionMode()) { + defaults.put("debug", true); + } + + if (deploymentConfiguration.isStandalone(request)) { + defaults.put("standalone", true); + } + + defaults.put("appUri", getAppUri(context)); + + return defaults; + } + + protected abstract String getAppUri(BootstrapContext context); + + /** + * Get the URI for the application theme. + * + * A portal-wide default theme is fetched from the portal shared resource + * directory (if any), other themes from the portlet. + * + * @param context + * @param themeName + * + * @return + */ + public String getThemeUri(BootstrapContext context, String themeName) { + WrappedRequest request = context.getRequest(); + final String staticFilePath = request.getDeploymentConfiguration() + .getStaticFileLocation(request); + return staticFilePath + "/" + + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName; + } + + /** + * Override if required + * + * @param context + * @return + */ + public String getThemeName(BootstrapContext context) { + return context.getApplication().getThemeForRoot(context.getRoot()); + } + + /** + * Don not override. + * + * @param context + * @return + */ + public String findAndEscapeThemeName(BootstrapContext context) { + String themeName = getThemeName(context); + if (themeName == null) { + WrappedRequest request = context.getRequest(); + themeName = request.getDeploymentConfiguration() + .getConfiguredTheme(request); + } + + // XSS preventation, theme names shouldn't contain special chars anyway. + // The servlet denies them via url parameter. + themeName = AbstractApplicationServlet.stripSpecialChars(themeName); + + return themeName; + } + + protected void writeError(WrappedResponse response, Throwable e) + throws IOException { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + e.getLocalizedMessage()); + } + + /** + * Gets the initial UIDL message to send to the client. + * + * @param request + * the originating request + * @param root + * the root for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws PaintException + * if an exception occurs while painting the components + * @throws JSONException + * if an exception occurs while formatting the output + */ + protected abstract String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException; + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java new file mode 100644 index 0000000000..d80e626cc1 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java @@ -0,0 +1,13 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventListener; + +public interface BootstrapListener extends EventListener { + public void modifyBootstrapFragment(BootstrapFragmentResponse response); + + public void modifyBootstrapPage(BootstrapPageResponse response); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java new file mode 100644 index 0000000000..802238ac62 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java @@ -0,0 +1,39 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.Map; + +import org.jsoup.nodes.Document; + +import com.vaadin.Application; +import com.vaadin.terminal.WrappedRequest; + +public class BootstrapPageResponse extends BootstrapResponse { + + private final Map<String, Object> headers; + private final Document document; + + public BootstrapPageResponse(BootstrapHandler handler, + WrappedRequest request, Document document, + Map<String, Object> headers, Application application, Integer rootId) { + super(handler, request, application, rootId); + this.headers = headers; + this.document = document; + } + + public void setHeader(String name, String value) { + headers.put(name, value); + } + + public void setDateHeader(String name, long timestamp) { + headers.put(name, Long.valueOf(timestamp)); + } + + public Document getDocument() { + return document; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java new file mode 100644 index 0000000000..88bd58593d --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java @@ -0,0 +1,45 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.EventObject; + +import com.vaadin.Application; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.ui.Root; + +public abstract class BootstrapResponse extends EventObject { + private final WrappedRequest request; + private final Application application; + private final Integer rootId; + + public BootstrapResponse(BootstrapHandler handler, WrappedRequest request, + Application application, Integer rootId) { + super(handler); + this.request = request; + this.application = application; + this.rootId = rootId; + } + + public BootstrapHandler getBootstrapHandler() { + return (BootstrapHandler) getSource(); + } + + public WrappedRequest getRequest() { + return request; + } + + public Application getApplication() { + return application; + } + + public Integer getRootId() { + return rootId; + } + + public Root getRoot() { + return Root.getCurrent(); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java b/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java new file mode 100644 index 0000000000..8f0c80332f --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java @@ -0,0 +1,39 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.util.Map; + +import com.vaadin.ui.AbstractComponent.ComponentErrorEvent; +import com.vaadin.ui.Component; + +@SuppressWarnings("serial") +public class ChangeVariablesErrorEvent implements ComponentErrorEvent { + + private Throwable throwable; + private Component component; + + private Map<String, Object> variableChanges; + + public ChangeVariablesErrorEvent(Component component, Throwable throwable, + Map<String, Object> variableChanges) { + this.component = component; + this.throwable = throwable; + this.variableChanges = variableChanges; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + public Component getComponent() { + return component; + } + + public Map<String, Object> getVariableChanges() { + return variableChanges; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java b/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java new file mode 100644 index 0000000000..4f74cfe4bb --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java @@ -0,0 +1,149 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.util.Collection; +import java.util.List; + +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.Extension; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.Root; + +/** + * Interface implemented by all connectors that are capable of communicating + * with the client side + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public interface ClientConnector extends Connector, RpcTarget { + /** + * Returns the list of pending server to client RPC calls and clears the + * list. + * + * @return an unmodifiable ordered list of pending server to client method + * calls (not null) + */ + public List<ClientMethodInvocation> retrievePendingRpcCalls(); + + /** + * Checks if the communicator is enabled. An enabled communicator is allowed + * to receive messages from its counter-part. + * + * @return true if the connector can receive messages, false otherwise + */ + public boolean isConnectorEnabled(); + + /** + * Returns the type of the shared state for this connector + * + * @return The type of the state. Must never return null. + */ + public Class<? extends SharedState> getStateType(); + + @Override + public ClientConnector getParent(); + + /** + * Requests that the connector should be repainted as soon as possible. + */ + public void requestRepaint(); + + /** + * Causes a repaint of this connector, and all connectors below it. + * + * This should only be used in special cases, e.g when the state of a + * descendant depends on the state of an ancestor. + */ + public void requestRepaintAll(); + + /** + * Sets the parent connector of the connector. + * + * <p> + * This method automatically calls {@link #attach()} if the connector + * becomes attached to the application, regardless of whether it was + * attached previously. Conversely, if the parent is {@code null} and the + * connector is attached to the application, {@link #detach()} is called for + * the connector. + * </p> + * <p> + * This method is rarely called directly. One of the + * {@link ComponentContainer#addComponent(Component)} or + * {@link AbstractClientConnector#addExtension(Extension)} methods are + * normally used for adding connectors to a parent and they will call this + * method implicitly. + * </p> + * + * <p> + * It is not possible to change the parent without first setting the parent + * to {@code null}. + * </p> + * + * @param parent + * the parent connector + * @throws IllegalStateException + * if a parent is given even though the connector already has a + * parent + */ + public void setParent(ClientConnector parent); + + /** + * Notifies the connector that it is connected to an application. + * + * <p> + * The caller of this method is {@link #setParent(ClientConnector)} if the + * parent is itself already attached to the application. If not, the parent + * will call the {@link #attach()} for all its children when it is attached + * to the application. This method is always called before the connector's + * data is sent to the client-side for the first time. + * </p> + * + * <p> + * The attachment logic is implemented in {@link AbstractClientConnector}. + * </p> + */ + public void attach(); + + /** + * Notifies the component that it is detached from the application. + * + * <p> + * The caller of this method is {@link #setParent(ClientConnector)} if the + * parent is in the application. When the parent is detached from the + * application it is its response to call {@link #detach()} for all the + * children and to detach itself from the terminal. + * </p> + */ + public void detach(); + + /** + * Get a read-only collection of all extensions attached to this connector. + * + * @return a collection of extensions + */ + public Collection<Extension> getExtensions(); + + /** + * Remove an extension from this connector. + * + * @param extension + * the extension to remove. + */ + public void removeExtension(Extension extension); + + /** + * Returns the root this connector is attached to + * + * @return The Root this connector is attached to or null if it is not + * attached to any Root + */ + public Root getRoot(); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java b/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java new file mode 100644 index 0000000000..64ea288665 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java @@ -0,0 +1,71 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * Internal class for keeping track of pending server to client method + * invocations for a Connector. + * + * @since 7.0 + */ +public class ClientMethodInvocation implements Serializable, + Comparable<ClientMethodInvocation> { + private final ClientConnector connector; + private final String interfaceName; + private final String methodName; + private final Object[] parameters; + private Type[] parameterTypes; + + // used for sorting calls between different connectors in the same Root + private final long sequenceNumber; + // TODO may cause problems when clustering etc. + private static long counter = 0; + + public ClientMethodInvocation(ClientConnector connector, + String interfaceName, Method method, Object[] parameters) { + this.connector = connector; + this.interfaceName = interfaceName; + methodName = method.getName(); + parameterTypes = method.getGenericParameterTypes(); + this.parameters = (null != parameters) ? parameters : new Object[0]; + sequenceNumber = ++counter; + } + + public Type[] getParameterTypes() { + return parameterTypes; + } + + public ClientConnector getConnector() { + return connector; + } + + public String getInterfaceName() { + return interfaceName; + } + + public String getMethodName() { + return methodName; + } + + public Object[] getParameters() { + return parameters; + } + + protected long getSequenceNumber() { + return sequenceNumber; + } + + @Override + public int compareTo(ClientMethodInvocation o) { + if (null == o) { + return 0; + } + return Long.signum(getSequenceNumber() - o.getSequenceNumber()); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java new file mode 100644 index 0000000000..3cc3a8cb64 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java @@ -0,0 +1,122 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.InputStream; +import java.net.URL; + +import javax.servlet.ServletContext; + +import com.vaadin.Application; +import com.vaadin.external.json.JSONException; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.ui.Root; + +/** + * Application manager processes changes and paints for single application + * instance. + * + * This class handles applications running as servlets. + * + * @see AbstractCommunicationManager + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class CommunicationManager extends AbstractCommunicationManager { + + /** + * @deprecated use {@link #CommunicationManager(Application)} instead + * @param application + * @param applicationServlet + */ + @Deprecated + public CommunicationManager(Application application, + AbstractApplicationServlet applicationServlet) { + super(application); + } + + /** + * TODO New constructor - document me! + * + * @param application + */ + public CommunicationManager(Application application) { + super(application); + } + + @Override + protected BootstrapHandler createBootstrapHandler() { + return new BootstrapHandler() { + @Override + protected String getApplicationId(BootstrapContext context) { + String appUrl = getAppUri(context); + + String appId = appUrl; + if ("".equals(appUrl)) { + appId = "ROOT"; + } + appId = appId.replaceAll("[^a-zA-Z0-9]", ""); + // Add hashCode to the end, so that it is still (sort of) + // predictable, but indicates that it should not be used in CSS + // and + // such: + int hashCode = appId.hashCode(); + if (hashCode < 0) { + hashCode = -hashCode; + } + appId = appId + "-" + hashCode; + return appId; + } + + @Override + protected String getAppUri(BootstrapContext context) { + /* Fetch relative url to application */ + // don't use server and port in uri. It may cause problems with + // some + // virtual server configurations which lose the server name + Application application = context.getApplication(); + URL url = application.getURL(); + String appUrl = url.getPath(); + if (appUrl.endsWith("/")) { + appUrl = appUrl.substring(0, appUrl.length() - 1); + } + return appUrl; + } + + @Override + public String getThemeName(BootstrapContext context) { + String themeName = context.getRequest().getParameter( + AbstractApplicationServlet.URL_PARAMETER_THEME); + if (themeName == null) { + themeName = super.getThemeName(context); + } + return themeName; + } + + @Override + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException { + return CommunicationManager.this.getInitialUIDL(request, root); + } + }; + } + + @Override + protected InputStream getThemeResourceAsStream(Root root, String themeName, + String resource) { + WebApplicationContext context = (WebApplicationContext) root + .getApplication().getContext(); + ServletContext servletContext = context.getHttpSession() + .getServletContext(); + return servletContext.getResourceAsStream("/" + + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName + + "/" + resource); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java b/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java new file mode 100644 index 0000000000..171d440796 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java @@ -0,0 +1,664 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.Vector; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.terminal.Sizeable.Unit; +import com.vaadin.ui.AbstractOrderedLayout; +import com.vaadin.ui.AbstractSplitPanel; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.Form; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.GridLayout.Area; +import com.vaadin.ui.Layout; +import com.vaadin.ui.Panel; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +@SuppressWarnings({ "serial", "deprecation" }) +public class ComponentSizeValidator implements Serializable { + + private final static int LAYERS_SHOWN = 4; + + /** + * Recursively checks given component and its subtree for invalid layout + * setups. Prints errors to std err stream. + * + * @param component + * component to check + * @return set of first level errors found + */ + public static List<InvalidLayout> validateComponentRelativeSizes( + Component component, List<InvalidLayout> errors, + InvalidLayout parent) { + + boolean invalidHeight = !checkHeights(component); + boolean invalidWidth = !checkWidths(component); + + if (invalidHeight || invalidWidth) { + InvalidLayout error = new InvalidLayout(component, invalidHeight, + invalidWidth); + if (parent != null) { + parent.addError(error); + } else { + if (errors == null) { + errors = new LinkedList<InvalidLayout>(); + } + errors.add(error); + } + parent = error; + } + + if (component instanceof Panel) { + Panel panel = (Panel) component; + errors = validateComponentRelativeSizes(panel.getContent(), errors, + parent); + } else if (component instanceof ComponentContainer) { + ComponentContainer lo = (ComponentContainer) component; + Iterator<Component> it = lo.getComponentIterator(); + while (it.hasNext()) { + errors = validateComponentRelativeSizes(it.next(), errors, + parent); + } + } else if (component instanceof Form) { + Form form = (Form) component; + if (form.getLayout() != null) { + errors = validateComponentRelativeSizes(form.getLayout(), + errors, parent); + } + if (form.getFooter() != null) { + errors = validateComponentRelativeSizes(form.getFooter(), + errors, parent); + } + } + + return errors; + } + + private static void printServerError(String msg, + Stack<ComponentInfo> attributes, boolean widthError, + PrintStream errorStream) { + StringBuffer err = new StringBuffer(); + err.append("Vaadin DEBUG\n"); + + StringBuilder indent = new StringBuilder(""); + ComponentInfo ci; + if (attributes != null) { + while (attributes.size() > LAYERS_SHOWN) { + attributes.pop(); + } + while (!attributes.empty()) { + ci = attributes.pop(); + showComponent(ci.component, ci.info, err, indent, widthError); + } + } + + err.append("Layout problem detected: "); + err.append(msg); + err.append("\n"); + err.append("Relative sizes were replaced by undefined sizes, components may not render as expected.\n"); + errorStream.println(err); + + } + + public static boolean checkHeights(Component component) { + try { + if (!hasRelativeHeight(component)) { + return true; + } + if (component instanceof Window) { + return true; + } + if (component.getParent() == null) { + return true; + } + + return parentCanDefineHeight(component); + } catch (Exception e) { + getLogger().log(Level.FINER, + "An exception occurred while validating sizes.", e); + return true; + } + } + + public static boolean checkWidths(Component component) { + try { + if (!hasRelativeWidth(component)) { + return true; + } + if (component instanceof Window) { + return true; + } + if (component.getParent() == null) { + return true; + } + + return parentCanDefineWidth(component); + } catch (Exception e) { + getLogger().log(Level.FINER, + "An exception occurred while validating sizes.", e); + return true; + } + } + + public static class InvalidLayout implements Serializable { + + private final Component component; + + private final boolean invalidHeight; + private final boolean invalidWidth; + + private final Vector<InvalidLayout> subErrors = new Vector<InvalidLayout>(); + + public InvalidLayout(Component component, boolean height, boolean width) { + this.component = component; + invalidHeight = height; + invalidWidth = width; + } + + public void addError(InvalidLayout error) { + subErrors.add(error); + } + + public void reportErrors(PrintWriter clientJSON, + AbstractCommunicationManager communicationManager, + PrintStream serverErrorStream) { + clientJSON.write("{"); + + Component parent = component.getParent(); + String paintableId = component.getConnectorId(); + + clientJSON.print("id:\"" + paintableId + "\""); + + if (invalidHeight) { + Stack<ComponentInfo> attributes = null; + String msg = ""; + // set proper error messages + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean vertical = false; + + if (ol instanceof VerticalLayout) { + vertical = true; + } + + if (vertical) { + msg = "Component with relative height inside a VerticalLayout with no height defined."; + attributes = getHeightAttributes(component); + } else { + msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined"; + attributes = getHeightAttributes(component); + } + } else if (parent instanceof GridLayout) { + msg = "At least one of the GridLayout's components in each row should have non relative height if the height of the layout is not defined."; + attributes = getHeightAttributes(component); + } else { + // default error for non sized parent issue + msg = "A component with relative height needs a parent with defined height."; + attributes = getHeightAttributes(component); + } + printServerError(msg, attributes, false, serverErrorStream); + clientJSON.print(",\"heightMsg\":\"" + msg + "\""); + } + if (invalidWidth) { + Stack<ComponentInfo> attributes = null; + String msg = ""; + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean horizontal = true; + + if (ol instanceof VerticalLayout) { + horizontal = false; + } + + if (horizontal) { + msg = "Component with relative width inside a HorizontalLayout with no width defined"; + attributes = getWidthAttributes(component); + } else { + msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined"; + attributes = getWidthAttributes(component); + } + } else if (parent instanceof GridLayout) { + msg = "At least one of the GridLayout's components in each column should have non relative width if the width of the layout is not defined."; + attributes = getWidthAttributes(component); + } else { + // default error for non sized parent issue + msg = "A component with relative width needs a parent with defined width."; + attributes = getWidthAttributes(component); + } + clientJSON.print(",\"widthMsg\":\"" + msg + "\""); + printServerError(msg, attributes, true, serverErrorStream); + } + if (subErrors.size() > 0) { + serverErrorStream.println("Sub errors >>"); + clientJSON.write(", \"subErrors\" : ["); + boolean first = true; + for (InvalidLayout subError : subErrors) { + if (!first) { + clientJSON.print(","); + } else { + first = false; + } + subError.reportErrors(clientJSON, communicationManager, + serverErrorStream); + } + clientJSON.write("]"); + serverErrorStream.println("<< Sub erros"); + } + clientJSON.write("}"); + } + } + + private static class ComponentInfo implements Serializable { + Component component; + String info; + + public ComponentInfo(Component component, String info) { + this.component = component; + this.info = info; + } + + } + + private static Stack<ComponentInfo> getHeightAttributes(Component component) { + Stack<ComponentInfo> attributes = new Stack<ComponentInfo>(); + attributes + .add(new ComponentInfo(component, getHeightString(component))); + Component parent = component.getParent(); + attributes.add(new ComponentInfo(parent, getHeightString(parent))); + + while ((parent = parent.getParent()) != null) { + attributes.add(new ComponentInfo(parent, getHeightString(parent))); + } + + return attributes; + } + + private static Stack<ComponentInfo> getWidthAttributes(Component component) { + Stack<ComponentInfo> attributes = new Stack<ComponentInfo>(); + attributes.add(new ComponentInfo(component, getWidthString(component))); + Component parent = component.getParent(); + attributes.add(new ComponentInfo(parent, getWidthString(parent))); + + while ((parent = parent.getParent()) != null) { + attributes.add(new ComponentInfo(parent, getWidthString(parent))); + } + + return attributes; + } + + private static String getWidthString(Component component) { + String width = "width: "; + if (hasRelativeWidth(component)) { + width += "RELATIVE, " + component.getWidth() + " %"; + } else if (component instanceof Window && component.getParent() == null) { + width += "MAIN WINDOW"; + } else if (component.getWidth() >= 0) { + width += "ABSOLUTE, " + component.getWidth() + " " + + component.getWidthUnits().getSymbol(); + } else { + width += "UNDEFINED"; + } + + return width; + } + + private static String getHeightString(Component component) { + String height = "height: "; + if (hasRelativeHeight(component)) { + height += "RELATIVE, " + component.getHeight() + " %"; + } else if (component instanceof Window && component.getParent() == null) { + height += "MAIN WINDOW"; + } else if (component.getHeight() > 0) { + height += "ABSOLUTE, " + component.getHeight() + " " + + component.getHeightUnits().getSymbol(); + } else { + height += "UNDEFINED"; + } + + return height; + } + + private static void showComponent(Component component, String attribute, + StringBuffer err, StringBuilder indent, boolean widthError) { + + FileLocation createLoc = creationLocations.get(component); + + FileLocation sizeLoc; + if (widthError) { + sizeLoc = widthLocations.get(component); + } else { + sizeLoc = heightLocations.get(component); + } + + err.append(indent); + indent.append(" "); + err.append("- "); + + err.append(component.getClass().getSimpleName()); + err.append("/").append(Integer.toHexString(component.hashCode())); + + if (component.getCaption() != null) { + err.append(" \""); + err.append(component.getCaption()); + err.append("\""); + } + + if (component.getDebugId() != null) { + err.append(" debugId: "); + err.append(component.getDebugId()); + } + + if (createLoc != null) { + err.append(", created at (" + createLoc.file + ":" + + createLoc.lineNumber + ")"); + + } + + if (attribute != null) { + err.append(" ("); + err.append(attribute); + if (sizeLoc != null) { + err.append(", set at (" + sizeLoc.file + ":" + + sizeLoc.lineNumber + ")"); + } + + err.append(")"); + } + err.append("\n"); + + } + + private static boolean hasNonRelativeHeightComponent( + AbstractOrderedLayout ol) { + Iterator<Component> it = ol.getComponentIterator(); + while (it.hasNext()) { + if (!hasRelativeHeight(it.next())) { + return true; + } + } + return false; + } + + public static boolean parentCanDefineHeight(Component component) { + Component parent = component.getParent(); + if (parent == null) { + // main window, valid situation + return true; + } + if (parent.getHeight() < 0) { + // Undefined height + if (parent instanceof Window) { + // Sub window with undefined size has a min-height + return true; + } + + if (parent instanceof AbstractOrderedLayout) { + boolean horizontal = true; + if (parent instanceof VerticalLayout) { + horizontal = false; + } + if (horizontal + && hasNonRelativeHeightComponent((AbstractOrderedLayout) parent)) { + return true; + } else { + return false; + } + + } else if (parent instanceof GridLayout) { + GridLayout gl = (GridLayout) parent; + Area componentArea = gl.getComponentArea(component); + boolean rowHasHeight = false; + for (int row = componentArea.getRow1(); !rowHasHeight + && row <= componentArea.getRow2(); row++) { + for (int column = 0; !rowHasHeight + && column < gl.getColumns(); column++) { + Component c = gl.getComponent(column, row); + if (c != null) { + rowHasHeight = !hasRelativeHeight(c); + } + } + } + if (!rowHasHeight) { + return false; + } else { + // Other components define row height + return true; + } + } + + if (parent instanceof Panel || parent instanceof AbstractSplitPanel + || parent instanceof TabSheet + || parent instanceof CustomComponent) { + // height undefined, we know how how component works and no + // exceptions + // TODO horiz SplitPanel ?? + return false; + } else { + // We cannot generally know if undefined component can serve + // space for children (like CustomLayout or component built by + // third party) so we assume they can + return true; + } + + } else if (hasRelativeHeight(parent)) { + // Relative height + if (parent.getParent() != null) { + return parentCanDefineHeight(parent); + } else { + return true; + } + } else { + // Absolute height + return true; + } + } + + private static boolean hasRelativeHeight(Component component) { + return (component.getHeightUnits() == Unit.PERCENTAGE && component + .getHeight() > 0); + } + + private static boolean hasNonRelativeWidthComponent(AbstractOrderedLayout ol) { + Iterator<Component> it = ol.getComponentIterator(); + while (it.hasNext()) { + if (!hasRelativeWidth(it.next())) { + return true; + } + } + return false; + } + + private static boolean hasRelativeWidth(Component paintable) { + return paintable.getWidth() > 0 + && paintable.getWidthUnits() == Unit.PERCENTAGE; + } + + public static boolean parentCanDefineWidth(Component component) { + Component parent = component.getParent(); + if (parent == null) { + // main window, valid situation + return true; + } + if (parent instanceof Window) { + // Sub window with undefined size has a min-width + return true; + } + + if (parent.getWidth() < 0) { + // Undefined width + + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean horizontal = true; + if (ol instanceof VerticalLayout) { + horizontal = false; + } + + if (!horizontal && hasNonRelativeWidthComponent(ol)) { + // valid situation, other components defined width + return true; + } else { + return false; + } + } else if (parent instanceof GridLayout) { + GridLayout gl = (GridLayout) parent; + Area componentArea = gl.getComponentArea(component); + boolean columnHasWidth = false; + for (int col = componentArea.getColumn1(); !columnHasWidth + && col <= componentArea.getColumn2(); col++) { + for (int row = 0; !columnHasWidth && row < gl.getRows(); row++) { + Component c = gl.getComponent(col, row); + if (c != null) { + columnHasWidth = !hasRelativeWidth(c); + } + } + } + if (!columnHasWidth) { + return false; + } else { + // Other components define column width + return true; + } + } else if (parent instanceof Form) { + /* + * If some other part of the form is not relative it determines + * the component width + */ + return hasNonRelativeWidthComponent((Form) parent); + } else if (parent instanceof AbstractSplitPanel + || parent instanceof TabSheet + || parent instanceof CustomComponent) { + // FIXME Could we use com.vaadin package name here and + // fail for all component containers? + // FIXME Actually this should be moved to containers so it can + // be implemented for custom containers + // TODO vertical splitpanel with another non relative component? + return false; + } else if (parent instanceof Window) { + // Sub window can define width based on caption + if (parent.getCaption() != null + && !parent.getCaption().equals("")) { + return true; + } else { + return false; + } + } else if (parent instanceof Panel) { + // TODO Panel should be able to define width based on caption + return false; + } else { + return true; + } + } else if (hasRelativeWidth(parent)) { + // Relative width + if (parent.getParent() == null) { + return true; + } + + return parentCanDefineWidth(parent); + } else { + return true; + } + + } + + private static boolean hasNonRelativeWidthComponent(Form form) { + Layout layout = form.getLayout(); + Layout footer = form.getFooter(); + + if (layout != null && !hasRelativeWidth(layout)) { + return true; + } + if (footer != null && !hasRelativeWidth(footer)) { + return true; + } + + return false; + } + + private static Map<Object, FileLocation> creationLocations = new HashMap<Object, FileLocation>(); + private static Map<Object, FileLocation> widthLocations = new HashMap<Object, FileLocation>(); + private static Map<Object, FileLocation> heightLocations = new HashMap<Object, FileLocation>(); + + public static class FileLocation implements Serializable { + public String method; + public String file; + public String className; + public String classNameSimple; + public int lineNumber; + + public FileLocation(StackTraceElement traceElement) { + file = traceElement.getFileName(); + className = traceElement.getClassName(); + classNameSimple = className + .substring(className.lastIndexOf('.') + 1); + lineNumber = traceElement.getLineNumber(); + method = traceElement.getMethodName(); + } + } + + public static void setCreationLocation(Object object) { + setLocation(creationLocations, object); + } + + public static void setWidthLocation(Object object) { + setLocation(widthLocations, object); + } + + public static void setHeightLocation(Object object) { + setLocation(heightLocations, object); + } + + private static void setLocation(Map<Object, FileLocation> map, Object object) { + StackTraceElement[] traceLines = Thread.currentThread().getStackTrace(); + for (StackTraceElement traceElement : traceLines) { + Class<?> cls; + try { + String className = traceElement.getClassName(); + if (className.startsWith("java.") + || className.startsWith("sun.")) { + continue; + } + + cls = Class.forName(className); + if (cls == ComponentSizeValidator.class || cls == Thread.class) { + continue; + } + + if (Component.class.isAssignableFrom(cls) + && !CustomComponent.class.isAssignableFrom(cls)) { + continue; + } + FileLocation cl = new FileLocation(traceElement); + map.put(object, cl); + return; + } catch (Exception e) { + // TODO Auto-generated catch block + getLogger().log(Level.FINER, + "An exception occurred while validating sizes.", e); + } + + } + } + + private static Logger getLogger() { + return Logger.getLogger(ComponentSizeValidator.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/Constants.java b/server/src/com/vaadin/terminal/gwt/server/Constants.java new file mode 100644 index 0000000000..7efb0205ac --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/Constants.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +/** + * TODO Document me! + * + * @author peholmst + * + */ +public interface Constants { + + static final String NOT_PRODUCTION_MODE_INFO = "\n" + + "=================================================================\n" + + "Vaadin is running in DEBUG MODE.\nAdd productionMode=true to web.xml " + + "to disable debug features.\nTo show debug window, add ?debug to " + + "your application URL.\n" + + "================================================================="; + + static final String WARNING_XSRF_PROTECTION_DISABLED = "\n" + + "===========================================================\n" + + "WARNING: Cross-site request forgery protection is disabled!\n" + + "==========================================================="; + + static final String WARNING_RESOURCE_CACHING_TIME_NOT_NUMERIC = "\n" + + "===========================================================\n" + + "WARNING: resourceCacheTime has been set to a non integer value " + + "in web.xml. The default of 1h will be used.\n" + + "==========================================================="; + + static final String WIDGETSET_MISMATCH_INFO = "\n" + + "=================================================================\n" + + "The widgetset in use does not seem to be built for the Vaadin\n" + + "version in use. This might cause strange problems - a\n" + + "recompile/deploy is strongly recommended.\n" + + " Vaadin version: %s\n" + + " Widgetset version: %s\n" + + "================================================================="; + + static final String URL_PARAMETER_RESTART_APPLICATION = "restartApplication"; + static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication"; + static final String URL_PARAMETER_REPAINT_ALL = "repaintAll"; + static final String URL_PARAMETER_THEME = "theme"; + + static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode"; + static final String SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION = "disable-xsrf-protection"; + static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime"; + + // Configurable parameter names + static final String PARAMETER_VAADIN_RESOURCES = "Resources"; + + static final int DEFAULT_BUFFER_SIZE = 32 * 1024; + + static final int MAX_BUFFER_SIZE = 64 * 1024; + + final String THEME_DIRECTORY_PATH = "VAADIN/themes/"; + + static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24; + + static final String WIDGETSET_DIRECTORY_PATH = "VAADIN/widgetsets/"; + + // Name of the default widget set, used if not specified in web.xml + static final String DEFAULT_WIDGETSET = "com.vaadin.terminal.gwt.DefaultWidgetSet"; + + // Widget set parameter name + static final String PARAMETER_WIDGETSET = "widgetset"; + + static final String ERROR_NO_ROOT_FOUND = "Application did not return a root for the request and did not request extra information either. Something is wrong."; + + static final String DEFAULT_THEME_NAME = "reindeer"; + + static final String INVALID_SECURITY_KEY_MSG = "Invalid security key."; + + // portal configuration parameters + static final String PORTAL_PARAMETER_VAADIN_WIDGETSET = "vaadin.widgetset"; + static final String PORTAL_PARAMETER_VAADIN_RESOURCE_PATH = "vaadin.resources.path"; + static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme"; + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java b/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java new file mode 100644 index 0000000000..efb5666efa --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java @@ -0,0 +1,313 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.vaadin.event.Transferable; +import com.vaadin.event.TransferableImpl; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.ui.dd.DragEventType; +import com.vaadin.terminal.Extension; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.ui.Component; +import com.vaadin.ui.Root; + +public class DragAndDropService implements VariableOwner, ClientConnector { + + private int lastVisitId; + + private boolean lastVisitAccepted = false; + + private DragAndDropEvent dragEvent; + + private final AbstractCommunicationManager manager; + + private AcceptCriterion acceptCriterion; + + public DragAndDropService(AbstractCommunicationManager manager) { + this.manager = manager; + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + Object owner = variables.get("dhowner"); + + // Validate drop handler owner + if (!(owner instanceof DropTarget)) { + getLogger() + .severe("DropHandler owner " + owner + + " must implement DropTarget"); + return; + } + // owner cannot be null here + + DropTarget dropTarget = (DropTarget) owner; + lastVisitId = (Integer) variables.get("visitId"); + + // request may be dropRequest or request during drag operation (commonly + // dragover or dragenter) + boolean dropRequest = isDropRequest(variables); + if (dropRequest) { + handleDropRequest(dropTarget, variables); + } else { + handleDragRequest(dropTarget, variables); + } + + } + + /** + * Handles a drop request from the VDragAndDropManager. + * + * @param dropTarget + * @param variables + */ + private void handleDropRequest(DropTarget dropTarget, + Map<String, Object> variables) { + DropHandler dropHandler = (dropTarget).getDropHandler(); + if (dropHandler == null) { + // No dropHandler returned so no drop can be performed. + getLogger().fine( + "DropTarget.getDropHandler() returned null for owner: " + + dropTarget); + return; + } + + /* + * Construct the Transferable and the DragDropDetails for the drop + * operation based on the info passed from the client widgets (drag + * source for Transferable, drop target for DragDropDetails). + */ + Transferable transferable = constructTransferable(dropTarget, variables); + TargetDetails dropData = constructDragDropDetails(dropTarget, variables); + DragAndDropEvent dropEvent = new DragAndDropEvent(transferable, + dropData); + if (dropHandler.getAcceptCriterion().accept(dropEvent)) { + dropHandler.drop(dropEvent); + } + } + + /** + * Handles a drag/move request from the VDragAndDropManager. + * + * @param dropTarget + * @param variables + */ + private void handleDragRequest(DropTarget dropTarget, + Map<String, Object> variables) { + lastVisitId = (Integer) variables.get("visitId"); + + acceptCriterion = dropTarget.getDropHandler().getAcceptCriterion(); + + /* + * Construct the Transferable and the DragDropDetails for the drag + * operation based on the info passed from the client widgets (drag + * source for Transferable, current target for DragDropDetails). + */ + Transferable transferable = constructTransferable(dropTarget, variables); + TargetDetails dragDropDetails = constructDragDropDetails(dropTarget, + variables); + + dragEvent = new DragAndDropEvent(transferable, dragDropDetails); + + lastVisitAccepted = acceptCriterion.accept(dragEvent); + } + + /** + * Construct DragDropDetails based on variables from client drop target. + * Uses DragDropDetailsTranslator if available, otherwise a default + * DragDropDetails implementation is used. + * + * @param dropTarget + * @param variables + * @return + */ + @SuppressWarnings("unchecked") + private TargetDetails constructDragDropDetails(DropTarget dropTarget, + Map<String, Object> variables) { + Map<String, Object> rawDragDropDetails = (Map<String, Object>) variables + .get("evt"); + + TargetDetails dropData = dropTarget + .translateDropTargetDetails(rawDragDropDetails); + + if (dropData == null) { + // Create a default DragDropDetails with all the raw variables + dropData = new TargetDetailsImpl(rawDragDropDetails, dropTarget); + } + + return dropData; + } + + private boolean isDropRequest(Map<String, Object> variables) { + return getRequestType(variables) == DragEventType.DROP; + } + + private DragEventType getRequestType(Map<String, Object> variables) { + int type = (Integer) variables.get("type"); + return DragEventType.values()[type]; + } + + @SuppressWarnings("unchecked") + private Transferable constructTransferable(DropTarget dropHandlerOwner, + Map<String, Object> variables) { + final Component sourceComponent = (Component) variables + .get("component"); + + variables = (Map<String, Object>) variables.get("tra"); + + Transferable transferable = null; + if (sourceComponent != null && sourceComponent instanceof DragSource) { + transferable = ((DragSource) sourceComponent) + .getTransferable(variables); + } + if (transferable == null) { + transferable = new TransferableImpl(sourceComponent, variables); + } + + return transferable; + } + + @Override + public boolean isEnabled() { + return isConnectorEnabled(); + } + + @Override + public boolean isImmediate() { + return true; + } + + void printJSONResponse(PrintWriter outWriter) throws PaintException { + if (isDirty()) { + + outWriter.print(", \"dd\":"); + + JsonPaintTarget jsonPaintTarget = new JsonPaintTarget(manager, + outWriter, false); + jsonPaintTarget.startTag("dd"); + jsonPaintTarget.addAttribute("visitId", lastVisitId); + if (acceptCriterion != null) { + jsonPaintTarget.addAttribute("accepted", lastVisitAccepted); + acceptCriterion.paintResponse(jsonPaintTarget); + } + jsonPaintTarget.endTag("dd"); + jsonPaintTarget.close(); + lastVisitId = -1; + lastVisitAccepted = false; + acceptCriterion = null; + dragEvent = null; + } + } + + private boolean isDirty() { + if (lastVisitId > 0) { + return true; + } + return false; + } + + @Override + public SharedState getState() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getConnectorId() { + return VDragAndDropManager.DD_SERVICE; + } + + @Override + public boolean isConnectorEnabled() { + // Drag'n'drop can't be disabled + return true; + } + + @Override + public List<ClientMethodInvocation> retrievePendingRpcCalls() { + return null; + } + + @Override + public RpcManager getRpcManager(Class<?> rpcInterface) { + // TODO Use rpc for drag'n'drop + return null; + } + + @Override + public Class<? extends SharedState> getStateType() { + return SharedState.class; + } + + @Override + public void requestRepaint() { + // TODO Auto-generated method stub + + } + + @Override + public ClientConnector getParent() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void requestRepaintAll() { + // TODO Auto-generated method stub + + } + + @Override + public void setParent(ClientConnector parent) { + // TODO Auto-generated method stub + + } + + @Override + public void attach() { + // TODO Auto-generated method stub + + } + + @Override + public void detach() { + // TODO Auto-generated method stub + + } + + @Override + public Collection<Extension> getExtensions() { + // TODO Auto-generated method stub + return Collections.emptySet(); + } + + @Override + public void removeExtension(Extension extension) { + // TODO Auto-generated method stub + } + + private Logger getLogger() { + return Logger.getLogger(DragAndDropService.class.getName()); + } + + @Override + public Root getRoot() { + return null; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java new file mode 100644 index 0000000000..cc12c9cc43 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java @@ -0,0 +1,417 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.google.appengine.api.datastore.Blob; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.FetchOptions.Builder; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.memcache.Expiration; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.apphosting.api.DeadlineExceededException; +import com.vaadin.service.ApplicationContext; + +/** + * ApplicationServlet to be used when deploying to Google App Engine, in + * web.xml: + * + * <pre> + * <servlet> + * <servlet-name>HelloWorld</servlet-name> + * <servlet-class>com.vaadin.terminal.gwt.server.GAEApplicationServlet</servlet-class> + * <init-param> + * <param-name>application</param-name> + * <param-value>com.vaadin.demo.HelloWorld</param-value> + * </init-param> + * </servlet> + * </pre> + * + * Session support must be enabled in appengine-web.xml: + * + * <pre> + * <sessions-enabled>true</sessions-enabled> + * </pre> + * + * Appengine datastore cleanup can be invoked by calling one of the applications + * with an additional path "/CLEAN". This can be set up as a cron-job in + * cron.xml (see appengine documentation for more information): + * + * <pre> + * <cronentries> + * <cron> + * <url>/HelloWorld/CLEAN</url> + * <description>Clean up sessions</description> + * <schedule>every 2 hours</schedule> + * </cron> + * </cronentries> + * </pre> + * + * It is recommended (but not mandatory) to extract themes and widgetsets and + * have App Engine server these statically. Extract VAADIN folder (and it's + * contents) 'next to' the WEB-INF folder, and add the following to + * appengine-web.xml: + * + * <pre> + * <static-files> + * <include path="/VAADIN/**" /> + * </static-files> + * </pre> + * + * Additional limitations: + * <ul> + * <li/>Do not change application state when serving an ApplicationResource. + * <li/>Avoid changing application state in transaction handlers, unless you're + * confident you fully understand the synchronization issues in App Engine. + * <li/>The application remains locked while uploading - no progressbar is + * possible. + * </ul> + */ +public class GAEApplicationServlet extends ApplicationServlet { + + // memcache mutex is MUTEX_BASE + sessio id + private static final String MUTEX_BASE = "_vmutex"; + + // used identify ApplicationContext in memcache and datastore + private static final String AC_BASE = "_vac"; + + // UIDL requests will attempt to gain access for this long before telling + // the client to retry + private static final int MAX_UIDL_WAIT_MILLISECONDS = 5000; + + // Tell client to retry after this delay. + // Note: currently interpreting Retry-After as ms, not sec + private static final int RETRY_AFTER_MILLISECONDS = 100; + + // Properties used in the datastore + private static final String PROPERTY_EXPIRES = "expires"; + private static final String PROPERTY_DATA = "data"; + + // path used for cleanup + private static final String CLEANUP_PATH = "/CLEAN"; + // max entities to clean at once + private static final int CLEANUP_LIMIT = 200; + // appengine session kind + private static final String APPENGINE_SESSION_KIND = "_ah_SESSION"; + // appengine session expires-parameter + private static final String PROPERTY_APPENGINE_EXPIRES = "_expires"; + + protected void sendDeadlineExceededNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + criticalNotification( + request, + response, + "Deadline Exceeded", + "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...", + "", null); + } + + protected void sendNotSerializableNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + criticalNotification( + request, + response, + "NotSerializableException", + "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", + "", getApplicationUrl(request).toString() + + "?restartApplication"); + } + + protected void sendCriticalErrorNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws IOException { + criticalNotification( + request, + response, + "Critical error", + "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", + "", getApplicationUrl(request).toString() + + "?restartApplication"); + } + + @Override + protected void service(HttpServletRequest unwrappedRequest, + HttpServletResponse unwrappedResponse) throws ServletException, + IOException { + WrappedHttpServletRequest request = new WrappedHttpServletRequest( + unwrappedRequest, getDeploymentConfiguration()); + WrappedHttpServletResponse response = new WrappedHttpServletResponse( + unwrappedResponse, getDeploymentConfiguration()); + + if (isCleanupRequest(request)) { + cleanDatastore(); + return; + } + + RequestType requestType = getRequestType(request); + + if (requestType == RequestType.STATIC_FILE) { + // no locking needed, let superclass handle + super.service(request, response); + cleanSession(request); + return; + } + + if (requestType == RequestType.APPLICATION_RESOURCE) { + // no locking needed, let superclass handle + getApplicationContext(request, + MemcacheServiceFactory.getMemcacheService()); + super.service(request, response); + cleanSession(request); + return; + } + + final HttpSession session = request + .getSession(requestCanCreateApplication(request, requestType)); + if (session == null) { + handleServiceSessionExpired(request, response); + cleanSession(request); + return; + } + + boolean locked = false; + MemcacheService memcache = null; + String mutex = MUTEX_BASE + session.getId(); + memcache = MemcacheServiceFactory.getMemcacheService(); + try { + // try to get lock + long started = new Date().getTime(); + // non-UIDL requests will try indefinitely + while (requestType != RequestType.UIDL + || new Date().getTime() - started < MAX_UIDL_WAIT_MILLISECONDS) { + locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + if (locked) { + break; + } + try { + Thread.sleep(RETRY_AFTER_MILLISECONDS); + } catch (InterruptedException e) { + getLogger().finer( + "Thread.sleep() interrupted while waiting for lock. Trying again. " + + e); + } + } + + if (!locked) { + // Not locked; only UIDL can get trough here unlocked: tell + // client to retry + response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + // Note: currently interpreting Retry-After as ms, not sec + response.setHeader("Retry-After", "" + RETRY_AFTER_MILLISECONDS); + return; + } + + // de-serialize or create application context, store in session + ApplicationContext ctx = getApplicationContext(request, memcache); + + super.service(request, response); + + // serialize + started = new Date().getTime(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(ctx); + oos.flush(); + byte[] bytes = baos.toByteArray(); + + started = new Date().getTime(); + + String id = AC_BASE + session.getId(); + Date expire = new Date(started + + (session.getMaxInactiveInterval() * 1000)); + Expiration expires = Expiration.onDate(expire); + + memcache.put(id, bytes, expires); + + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Entity entity = new Entity(AC_BASE, id); + entity.setProperty(PROPERTY_EXPIRES, expire.getTime()); + entity.setProperty(PROPERTY_DATA, new Blob(bytes)); + ds.put(entity); + + } catch (DeadlineExceededException e) { + getLogger().warning("DeadlineExceeded for " + session.getId()); + sendDeadlineExceededNotification(request, response); + } catch (NotSerializableException e) { + getLogger().log(Level.SEVERE, "Not serializable!", e); + + // TODO this notification is usually not shown - should we redirect + // in some other way - can we? + sendNotSerializableNotification(request, response); + } catch (Exception e) { + getLogger().log(Level.WARNING, + "An exception occurred while servicing request.", e); + + sendCriticalErrorNotification(request, response); + } finally { + // "Next, please!" + if (locked) { + memcache.delete(mutex); + } + cleanSession(request); + } + } + + protected ApplicationContext getApplicationContext( + HttpServletRequest request, MemcacheService memcache) { + HttpSession session = request.getSession(); + String id = AC_BASE + session.getId(); + byte[] serializedAC = (byte[]) memcache.get(id); + if (serializedAC == null) { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Key key = KeyFactory.createKey(AC_BASE, id); + Entity entity = null; + try { + entity = ds.get(key); + } catch (EntityNotFoundException e) { + // Ok, we were a bit optimistic; we'll create a new one later + } + if (entity != null) { + Blob blob = (Blob) entity.getProperty(PROPERTY_DATA); + serializedAC = blob.getBytes(); + // bring it to memcache + memcache.put(AC_BASE + session.getId(), serializedAC, + Expiration.byDeltaSeconds(session + .getMaxInactiveInterval()), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + } + } + if (serializedAC != null) { + ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC); + ObjectInputStream ois; + try { + ois = new ObjectInputStream(bais); + ApplicationContext applicationContext = (ApplicationContext) ois + .readObject(); + session.setAttribute(WebApplicationContext.class.getName(), + applicationContext); + } catch (IOException e) { + getLogger().log( + Level.WARNING, + "Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created. ", e); + } catch (ClassNotFoundException e) { + getLogger().log( + Level.WARNING, + "Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created. ", e); + } + } + // will create new context if the above did not + return getApplicationContext(session); + + } + + private boolean isCleanupRequest(HttpServletRequest request) { + String path = getRequestPathInfo(request); + if (path != null && path.equals(CLEANUP_PATH)) { + return true; + } + return false; + } + + /** + * Removes the ApplicationContext from the session in order to minimize the + * data serialized to datastore and memcache. + * + * @param request + */ + private void cleanSession(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.removeAttribute(WebApplicationContext.class.getName()); + } + } + + /** + * This will look at the timestamp and delete expired persisted Vaadin and + * appengine sessions from the datastore. + * + * TODO Possible improvements include: 1. Use transactions (requires entity + * groups - overkill?) 2. Delete one-at-a-time, catch possible exception, + * continue w/ next. + */ + private void cleanDatastore() { + long expire = new Date().getTime(); + try { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + // Vaadin stuff first + { + Query q = new Query(AC_BASE); + q.setKeysOnly(); + + q.addFilter(PROPERTY_EXPIRES, + FilterOperator.LESS_THAN_OR_EQUAL, expire); + PreparedQuery pq = ds.prepare(q); + List<Entity> entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired Vaadin sessions."); + List<Key> keys = new ArrayList<Key>(); + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + } + // Also cleanup GAE sessions + { + Query q = new Query(APPENGINE_SESSION_KIND); + q.setKeysOnly(); + q.addFilter(PROPERTY_APPENGINE_EXPIRES, + FilterOperator.LESS_THAN_OR_EQUAL, expire); + PreparedQuery pq = ds.prepare(q); + List<Entity> entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired appengine sessions."); + List<Key> keys = new ArrayList<Key>(); + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + } + } catch (Exception e) { + getLogger().log(Level.WARNING, "Exception while cleaning.", e); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(GAEApplicationServlet.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java b/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java new file mode 100644 index 0000000000..d811cadf86 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java @@ -0,0 +1,54 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import javax.servlet.Filter; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext.TransactionListener; +import com.vaadin.terminal.Terminal; + +/** + * {@link Application} that implements this interface gets notified of request + * start and end by terminal. + * <p> + * Interface can be used for several helper tasks including: + * <ul> + * <li>Opening and closing database connections + * <li>Implementing {@link ThreadLocal} + * <li>Setting/Getting {@link Cookie} + * </ul> + * <p> + * Alternatives for implementing similar features are are Servlet {@link Filter} + * s and {@link TransactionListener}s in Vaadin. + * + * @since 6.2 + * @see PortletRequestListener + */ +public interface HttpServletRequestListener extends Serializable { + + /** + * This method is called before {@link Terminal} applies the request to + * Application. + * + * @param request + * @param response + */ + public void onRequestStart(HttpServletRequest request, + HttpServletResponse response); + + /** + * This method is called at the end of each request. + * + * @param request + * @param response + */ + public void onRequestEnd(HttpServletRequest request, + HttpServletResponse response); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java b/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java new file mode 100644 index 0000000000..8199bc6ada --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java @@ -0,0 +1,792 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.terminal.gwt.client.communication.JsonEncoder; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; + +/** + * Decoder for converting RPC parameters and other values from JSON in transfer + * between the client and the server and vice versa. + * + * @since 7.0 + */ +public class JsonCodec implements Serializable { + + private static Map<Class<?>, String> typeToTransportType = new HashMap<Class<?>, String>(); + + /** + * Note! This does not contain primitives. + * <p> + */ + private static Map<String, Class<?>> transportTypeToType = new HashMap<String, Class<?>>(); + + static { + registerType(String.class, JsonEncoder.VTYPE_STRING); + registerType(Connector.class, JsonEncoder.VTYPE_CONNECTOR); + registerType(Boolean.class, JsonEncoder.VTYPE_BOOLEAN); + registerType(boolean.class, JsonEncoder.VTYPE_BOOLEAN); + registerType(Integer.class, JsonEncoder.VTYPE_INTEGER); + registerType(int.class, JsonEncoder.VTYPE_INTEGER); + registerType(Float.class, JsonEncoder.VTYPE_FLOAT); + registerType(float.class, JsonEncoder.VTYPE_FLOAT); + registerType(Double.class, JsonEncoder.VTYPE_DOUBLE); + registerType(double.class, JsonEncoder.VTYPE_DOUBLE); + registerType(Long.class, JsonEncoder.VTYPE_LONG); + registerType(long.class, JsonEncoder.VTYPE_LONG); + registerType(String[].class, JsonEncoder.VTYPE_STRINGARRAY); + registerType(Object[].class, JsonEncoder.VTYPE_ARRAY); + registerType(Map.class, JsonEncoder.VTYPE_MAP); + registerType(HashMap.class, JsonEncoder.VTYPE_MAP); + registerType(List.class, JsonEncoder.VTYPE_LIST); + registerType(Set.class, JsonEncoder.VTYPE_SET); + } + + private static void registerType(Class<?> type, String transportType) { + typeToTransportType.put(type, transportType); + if (!type.isPrimitive()) { + transportTypeToType.put(transportType, type); + } + } + + public static boolean isInternalTransportType(String transportType) { + return transportTypeToType.containsKey(transportType); + } + + public static boolean isInternalType(Type type) { + if (type instanceof Class && ((Class<?>) type).isPrimitive()) { + if (type == byte.class || type == char.class) { + // Almost all primitive types are handled internally + return false; + } + // All primitive types are handled internally + return true; + } else if (type == UidlValue.class) { + // UidlValue is a special internal type wrapping type info and a + // value + return true; + } + return typeToTransportType.containsKey(getClassForType(type)); + } + + private static Class<?> getClassForType(Type type) { + if (type instanceof ParameterizedType) { + return (Class<?>) (((ParameterizedType) type).getRawType()); + } else if (type instanceof Class<?>) { + return (Class<?>) type; + } else { + return null; + } + } + + private static Class<?> getType(String transportType) { + return transportTypeToType.get(transportType); + } + + public static Object decodeInternalOrCustomType(Type targetType, + Object value, ConnectorTracker connectorTracker) + throws JSONException { + if (isInternalType(targetType)) { + return decodeInternalType(targetType, false, value, + connectorTracker); + } else { + return decodeCustomType(targetType, value, connectorTracker); + } + } + + public static Object decodeCustomType(Type targetType, Object value, + ConnectorTracker connectorTracker) throws JSONException { + if (isInternalType(targetType)) { + throw new JSONException("decodeCustomType cannot be used for " + + targetType + ", which is an internal type"); + } + + // Try to decode object using fields + if (value == JSONObject.NULL) { + return null; + } else if (targetType == byte.class || targetType == Byte.class) { + return Byte.valueOf(String.valueOf(value)); + } else if (targetType == char.class || targetType == Character.class) { + return Character.valueOf(String.valueOf(value).charAt(0)); + } else if (targetType instanceof Class<?> + && ((Class<?>) targetType).isArray()) { + // Legacy Object[] and String[] handled elsewhere, this takes care + // of generic arrays + Class<?> componentType = ((Class<?>) targetType).getComponentType(); + return decodeArray(componentType, (JSONArray) value, + connectorTracker); + } else if (targetType instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) targetType) + .getGenericComponentType(); + return decodeArray(componentType, (JSONArray) value, + connectorTracker); + } else if (targetType == JSONObject.class + || targetType == JSONArray.class) { + return value; + } else { + return decodeObject(targetType, (JSONObject) value, + connectorTracker); + } + } + + private static Object decodeArray(Type componentType, JSONArray value, + ConnectorTracker connectorTracker) throws JSONException { + Class<?> componentClass = getClassForType(componentType); + Object array = Array.newInstance(componentClass, value.length()); + for (int i = 0; i < value.length(); i++) { + Object decodedValue = decodeInternalOrCustomType(componentType, + value.get(i), connectorTracker); + Array.set(array, i, decodedValue); + } + return array; + } + + /** + * Decodes a value that is of an internal type. + * <p> + * Ensures the encoded value is of the same type as target type. + * </p> + * <p> + * Allows restricting collections so that they must be declared using + * generics. If this is used then all objects in the collection are encoded + * using the declared type. Otherwise only internal types are allowed in + * collections. + * </p> + * + * @param targetType + * The type that should be returned by this method + * @param valueAndType + * The encoded value and type array + * @param application + * A reference to the application + * @param enforceGenericsInCollections + * true if generics should be enforce, false to only allow + * internal types in collections + * @return + * @throws JSONException + */ + public static Object decodeInternalType(Type targetType, + boolean restrictToInternalTypes, Object encodedJsonValue, + ConnectorTracker connectorTracker) throws JSONException { + if (!isInternalType(targetType)) { + throw new JSONException("Type " + targetType + + " is not a supported internal type."); + } + String transportType = getInternalTransportType(targetType); + + if (encodedJsonValue == JSONObject.NULL) { + return null; + } + + // UidlValue + if (targetType == UidlValue.class) { + return decodeUidlValue((JSONArray) encodedJsonValue, + connectorTracker); + } + + // Collections + if (JsonEncoder.VTYPE_LIST.equals(transportType)) { + return decodeList(targetType, restrictToInternalTypes, + (JSONArray) encodedJsonValue, connectorTracker); + } else if (JsonEncoder.VTYPE_SET.equals(transportType)) { + return decodeSet(targetType, restrictToInternalTypes, + (JSONArray) encodedJsonValue, connectorTracker); + } else if (JsonEncoder.VTYPE_MAP.equals(transportType)) { + return decodeMap(targetType, restrictToInternalTypes, + encodedJsonValue, connectorTracker); + } + + // Arrays + if (JsonEncoder.VTYPE_ARRAY.equals(transportType)) { + + return decodeObjectArray(targetType, (JSONArray) encodedJsonValue, + connectorTracker); + + } else if (JsonEncoder.VTYPE_STRINGARRAY.equals(transportType)) { + return decodeStringArray((JSONArray) encodedJsonValue); + } + + // Special Vaadin types + + String stringValue = String.valueOf(encodedJsonValue); + + if (JsonEncoder.VTYPE_CONNECTOR.equals(transportType)) { + return connectorTracker.getConnector(stringValue); + } + + // Legacy types + + if (JsonEncoder.VTYPE_STRING.equals(transportType)) { + return stringValue; + } else if (JsonEncoder.VTYPE_INTEGER.equals(transportType)) { + return Integer.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_LONG.equals(transportType)) { + return Long.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_FLOAT.equals(transportType)) { + return Float.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_DOUBLE.equals(transportType)) { + return Double.valueOf(stringValue); + } else if (JsonEncoder.VTYPE_BOOLEAN.equals(transportType)) { + return Boolean.valueOf(stringValue); + } + + throw new JSONException("Unknown type " + transportType); + } + + private static UidlValue decodeUidlValue(JSONArray encodedJsonValue, + ConnectorTracker connectorTracker) throws JSONException { + String type = encodedJsonValue.getString(0); + + Object decodedValue = decodeInternalType(getType(type), true, + encodedJsonValue.get(1), connectorTracker); + return new UidlValue(decodedValue); + } + + private static boolean transportTypesCompatible( + String encodedTransportType, String transportType) { + if (encodedTransportType == null) { + return false; + } + if (encodedTransportType.equals(transportType)) { + return true; + } + if (encodedTransportType.equals(JsonEncoder.VTYPE_NULL)) { + return true; + } + + return false; + } + + private static Map<Object, Object> decodeMap(Type targetType, + boolean restrictToInternalTypes, Object jsonMap, + ConnectorTracker connectorTracker) throws JSONException { + if (jsonMap instanceof JSONArray) { + // Client-side has no declared type information to determine + // encoding method for empty maps, so these are handled separately. + // See #8906. + JSONArray jsonArray = (JSONArray) jsonMap; + if (jsonArray.length() == 0) { + return new HashMap<Object, Object>(); + } + } + + if (!restrictToInternalTypes && targetType instanceof ParameterizedType) { + Type keyType = ((ParameterizedType) targetType) + .getActualTypeArguments()[0]; + Type valueType = ((ParameterizedType) targetType) + .getActualTypeArguments()[1]; + if (keyType == String.class) { + return decodeStringMap(valueType, (JSONObject) jsonMap, + connectorTracker); + } else if (keyType == Connector.class) { + return decodeConnectorMap(valueType, (JSONObject) jsonMap, + connectorTracker); + } else { + return decodeObjectMap(keyType, valueType, (JSONArray) jsonMap, + connectorTracker); + } + } else { + return decodeStringMap(UidlValue.class, (JSONObject) jsonMap, + connectorTracker); + } + } + + private static Map<Object, Object> decodeObjectMap(Type keyType, + Type valueType, JSONArray jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map<Object, Object> map = new HashMap<Object, Object>(); + + JSONArray keys = jsonMap.getJSONArray(0); + JSONArray values = jsonMap.getJSONArray(1); + + assert (keys.length() == values.length()); + + for (int i = 0; i < keys.length(); i++) { + Object key = decodeInternalOrCustomType(keyType, keys.get(i), + connectorTracker); + Object value = decodeInternalOrCustomType(valueType, values.get(i), + connectorTracker); + + map.put(key, value); + } + + return map; + } + + private static Map<Object, Object> decodeConnectorMap(Type valueType, + JSONObject jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map<Object, Object> map = new HashMap<Object, Object>(); + + for (Iterator<?> iter = jsonMap.keys(); iter.hasNext();) { + String key = (String) iter.next(); + Object value = decodeInternalOrCustomType(valueType, + jsonMap.get(key), connectorTracker); + if (valueType == UidlValue.class) { + value = ((UidlValue) value).getValue(); + } + map.put(connectorTracker.getConnector(key), value); + } + + return map; + } + + private static Map<Object, Object> decodeStringMap(Type valueType, + JSONObject jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map<Object, Object> map = new HashMap<Object, Object>(); + + for (Iterator<?> iter = jsonMap.keys(); iter.hasNext();) { + String key = (String) iter.next(); + Object value = decodeInternalOrCustomType(valueType, + jsonMap.get(key), connectorTracker); + if (valueType == UidlValue.class) { + value = ((UidlValue) value).getValue(); + } + map.put(key, value); + } + + return map; + } + + /** + * @param targetType + * @param restrictToInternalTypes + * @param typeIndex + * The index of a generic type to use to define the child type + * that should be decoded + * @param encodedValueAndType + * @param application + * @return + * @throws JSONException + */ + private static Object decodeParametrizedType(Type targetType, + boolean restrictToInternalTypes, int typeIndex, Object value, + ConnectorTracker connectorTracker) throws JSONException { + if (!restrictToInternalTypes && targetType instanceof ParameterizedType) { + Type childType = ((ParameterizedType) targetType) + .getActualTypeArguments()[typeIndex]; + // Only decode the given type + return decodeInternalOrCustomType(childType, value, + connectorTracker); + } else { + // Only UidlValue when not enforcing a given type to avoid security + // issues + UidlValue decodeInternalType = (UidlValue) decodeInternalType( + UidlValue.class, true, value, connectorTracker); + return decodeInternalType.getValue(); + } + } + + private static Object decodeEnum(Class<? extends Enum> cls, JSONObject value) { + String enumIdentifier = String.valueOf(value); + return Enum.valueOf(cls, enumIdentifier); + } + + private static String[] decodeStringArray(JSONArray jsonArray) + throws JSONException { + int length = jsonArray.length(); + List<String> tokens = new ArrayList<String>(length); + for (int i = 0; i < length; ++i) { + tokens.add(jsonArray.getString(i)); + } + return tokens.toArray(new String[tokens.size()]); + } + + private static Object[] decodeObjectArray(Type targetType, + JSONArray jsonArray, ConnectorTracker connectorTracker) + throws JSONException { + List list = decodeList(List.class, true, jsonArray, connectorTracker); + return list.toArray(new Object[list.size()]); + } + + private static List<Object> decodeList(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, + ConnectorTracker connectorTracker) throws JSONException { + List<Object> list = new ArrayList<Object>(); + for (int i = 0; i < jsonArray.length(); ++i) { + // each entry always has two elements: type and value + Object encodedValue = jsonArray.get(i); + Object decodedChild = decodeParametrizedType(targetType, + restrictToInternalTypes, 0, encodedValue, connectorTracker); + list.add(decodedChild); + } + return list; + } + + private static Set<Object> decodeSet(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, + ConnectorTracker connectorTracker) throws JSONException { + HashSet<Object> set = new HashSet<Object>(); + set.addAll(decodeList(targetType, restrictToInternalTypes, jsonArray, + connectorTracker)); + return set; + } + + /** + * Returns the name that should be used as field name in the JSON. We strip + * "set" from the setter, keeping the result - this is easy to do on both + * server and client, avoiding some issues with cASE. E.g setZIndex() + * becomes "zIndex". Also ensures that both getter and setter are present, + * returning null otherwise. + * + * @param pd + * @return the name to be used or null if both getter and setter are not + * found. + */ + static String getTransportFieldName(PropertyDescriptor pd) { + if (pd.getReadMethod() == null || pd.getWriteMethod() == null) { + return null; + } + String fieldName = pd.getWriteMethod().getName().substring(3); + fieldName = Character.toLowerCase(fieldName.charAt(0)) + + fieldName.substring(1); + return fieldName; + } + + private static Object decodeObject(Type targetType, + JSONObject serializedObject, ConnectorTracker connectorTracker) + throws JSONException { + + Class<?> targetClass = getClassForType(targetType); + if (Enum.class.isAssignableFrom(targetClass)) { + return decodeEnum(targetClass.asSubclass(Enum.class), + serializedObject); + } + + try { + Object decodedObject = targetClass.newInstance(); + for (PropertyDescriptor pd : Introspector.getBeanInfo(targetClass) + .getPropertyDescriptors()) { + + String fieldName = getTransportFieldName(pd); + if (fieldName == null) { + continue; + } + Object encodedFieldValue = serializedObject.get(fieldName); + Type fieldType = pd.getReadMethod().getGenericReturnType(); + Object decodedFieldValue = decodeInternalOrCustomType( + fieldType, encodedFieldValue, connectorTracker); + + pd.getWriteMethod().invoke(decodedObject, decodedFieldValue); + } + + return decodedObject; + } catch (IllegalArgumentException e) { + throw new JSONException(e); + } catch (IllegalAccessException e) { + throw new JSONException(e); + } catch (InvocationTargetException e) { + throw new JSONException(e); + } catch (InstantiationException e) { + throw new JSONException(e); + } catch (IntrospectionException e) { + throw new JSONException(e); + } + } + + public static Object encode(Object value, Object referenceValue, + Type valueType, ConnectorTracker connectorTracker) + throws JSONException { + + if (valueType == null) { + throw new IllegalArgumentException("type must be defined"); + } + + if (valueType instanceof WildcardType) { + throw new IllegalStateException( + "Can not serialize type with wildcard: " + valueType); + } + + if (null == value) { + return encodeNull(); + } + + if (value instanceof String[]) { + String[] array = (String[]) value; + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < array.length; ++i) { + jsonArray.put(array[i]); + } + return jsonArray; + } else if (value instanceof String) { + return value; + } else if (value instanceof Boolean) { + return value; + } else if (value instanceof Number) { + return value; + } else if (value instanceof Character) { + // Character is not a Number + return value; + } else if (value instanceof Collection) { + Collection<?> collection = (Collection<?>) value; + JSONArray jsonArray = encodeCollection(valueType, collection, + connectorTracker); + return jsonArray; + } else if (valueType instanceof Class<?> + && ((Class<?>) valueType).isArray()) { + JSONArray jsonArray = encodeArrayContents( + ((Class<?>) valueType).getComponentType(), value, + connectorTracker); + return jsonArray; + } else if (valueType instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) valueType) + .getGenericComponentType(); + JSONArray jsonArray = encodeArrayContents(componentType, value, + connectorTracker); + return jsonArray; + } else if (value instanceof Map) { + Object jsonMap = encodeMap(valueType, (Map<?, ?>) value, + connectorTracker); + return jsonMap; + } else if (value instanceof Connector) { + Connector connector = (Connector) value; + if (value instanceof Component + && !(AbstractCommunicationManager + .isVisible((Component) value))) { + return encodeNull(); + } + return connector.getConnectorId(); + } else if (value instanceof Enum) { + return encodeEnum((Enum<?>) value, connectorTracker); + } else if (value instanceof JSONArray || value instanceof JSONObject) { + return value; + } else { + // Any object that we do not know how to encode we encode by looping + // through fields + return encodeObject(value, referenceValue, connectorTracker); + } + } + + private static Object encodeNull() { + return JSONObject.NULL; + } + + private static Object encodeObject(Object value, Object referenceValue, + ConnectorTracker connectorTracker) throws JSONException { + JSONObject jsonMap = new JSONObject(); + + try { + for (PropertyDescriptor pd : Introspector.getBeanInfo( + value.getClass()).getPropertyDescriptors()) { + String fieldName = getTransportFieldName(pd); + if (fieldName == null) { + continue; + } + Method getterMethod = pd.getReadMethod(); + // We can't use PropertyDescriptor.getPropertyType() as it does + // not support generics + Type fieldType = getterMethod.getGenericReturnType(); + Object fieldValue = getterMethod.invoke(value, (Object[]) null); + boolean equals = false; + Object referenceFieldValue = null; + if (referenceValue != null) { + referenceFieldValue = getterMethod.invoke(referenceValue, + (Object[]) null); + equals = equals(fieldValue, referenceFieldValue); + } + if (!equals) { + if (jsonMap.has(fieldName)) { + throw new RuntimeException( + "Can't encode " + + value.getClass().getName() + + " as it has multiple fields with the name " + + fieldName.toLowerCase() + + ". This can happen if only casing distinguishes one property name from another."); + } + jsonMap.put( + fieldName, + encode(fieldValue, referenceFieldValue, fieldType, + connectorTracker)); + // } else { + // System.out.println("Skipping field " + fieldName + // + " of type " + fieldType.getName() + // + " for object " + value.getClass().getName() + // + " as " + fieldValue + "==" + referenceFieldValue); + } + } + } catch (Exception e) { + // TODO: Should exceptions be handled in a different way? + throw new JSONException(e); + } + return jsonMap; + } + + /** + * Compares the value with the reference. If they match, returns true. + * + * @param fieldValue + * @param referenceValue + * @return + */ + private static boolean equals(Object fieldValue, Object referenceValue) { + if (fieldValue == null) { + return referenceValue == null; + } + + if (fieldValue.equals(referenceValue)) { + return true; + } + + return false; + } + + private static String encodeEnum(Enum<?> e, + ConnectorTracker connectorTracker) throws JSONException { + return e.name(); + } + + private static JSONArray encodeArrayContents(Type componentType, + Object array, ConnectorTracker connectorTracker) + throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < Array.getLength(array); i++) { + jsonArray.put(encode(Array.get(array, i), null, componentType, + connectorTracker)); + } + return jsonArray; + } + + private static JSONArray encodeCollection(Type targetType, + Collection collection, ConnectorTracker connectorTracker) + throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (Object o : collection) { + jsonArray.put(encodeChild(targetType, 0, o, connectorTracker)); + } + return jsonArray; + } + + private static Object encodeChild(Type targetType, int typeIndex, Object o, + ConnectorTracker connectorTracker) throws JSONException { + if (targetType instanceof ParameterizedType) { + Type childType = ((ParameterizedType) targetType) + .getActualTypeArguments()[typeIndex]; + // Encode using the given type + return encode(o, null, childType, connectorTracker); + } else { + throw new JSONException("Collection is missing generics"); + } + } + + private static Object encodeMap(Type mapType, Map<?, ?> map, + ConnectorTracker connectorTracker) throws JSONException { + Type keyType, valueType; + + if (mapType instanceof ParameterizedType) { + keyType = ((ParameterizedType) mapType).getActualTypeArguments()[0]; + valueType = ((ParameterizedType) mapType).getActualTypeArguments()[1]; + } else { + throw new JSONException("Map is missing generics"); + } + + if (map.isEmpty()) { + // Client -> server encodes empty map as an empty array because of + // #8906. Do the same for server -> client to maintain symmetry. + return new JSONArray(); + } + + if (keyType == String.class) { + return encodeStringMap(valueType, map, connectorTracker); + } else if (keyType == Connector.class) { + return encodeConnectorMap(valueType, map, connectorTracker); + } else { + return encodeObjectMap(keyType, valueType, map, connectorTracker); + } + } + + private static JSONArray encodeObjectMap(Type keyType, Type valueType, + Map<?, ?> map, ConnectorTracker connectorTracker) + throws JSONException { + JSONArray keys = new JSONArray(); + JSONArray values = new JSONArray(); + + for (Entry<?, ?> entry : map.entrySet()) { + Object encodedKey = encode(entry.getKey(), null, keyType, + connectorTracker); + Object encodedValue = encode(entry.getValue(), null, valueType, + connectorTracker); + + keys.put(encodedKey); + values.put(encodedValue); + } + + return new JSONArray(Arrays.asList(keys, values)); + } + + private static JSONObject encodeConnectorMap(Type valueType, Map<?, ?> map, + ConnectorTracker connectorTracker) throws JSONException { + JSONObject jsonMap = new JSONObject(); + + for (Entry<?, ?> entry : map.entrySet()) { + Connector key = (Connector) entry.getKey(); + Object encodedValue = encode(entry.getValue(), null, valueType, + connectorTracker); + jsonMap.put(key.getConnectorId(), encodedValue); + } + + return jsonMap; + } + + private static JSONObject encodeStringMap(Type valueType, Map<?, ?> map, + ConnectorTracker connectorTracker) throws JSONException { + JSONObject jsonMap = new JSONObject(); + + for (Entry<?, ?> entry : map.entrySet()) { + String key = (String) entry.getKey(); + Object encodedValue = encode(entry.getValue(), null, valueType, + connectorTracker); + jsonMap.put(key, encodedValue); + } + + return jsonMap; + } + + /** + * Gets the transport type for the given class. Returns null if no transport + * type can be found. + * + * @param valueType + * The type that should be transported + * @return + * @throws JSONException + */ + private static String getInternalTransportType(Type valueType) { + return typeToTransportType.get(getClassForType(valueType)); + } + + private static String getCustomTransportType(Class<?> targetType) { + return targetType.getName(); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java b/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java new file mode 100644 index 0000000000..5a830ddb63 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java @@ -0,0 +1,1022 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.Vector; +import java.util.logging.Logger; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.StreamVariable; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Component; +import com.vaadin.ui.CustomLayout; + +/** + * User Interface Description Language Target. + * + * TODO document better: role of this class, UIDL format, attributes, variables, + * etc. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class JsonPaintTarget implements PaintTarget { + + /* Document type declarations */ + + private final static String UIDL_ARG_NAME = "name"; + + private final Stack<String> mOpenTags; + + private final Stack<JsonTag> openJsonTags; + + // these match each other element-wise + private final Stack<ClientConnector> openPaintables; + private final Stack<String> openPaintableTags; + + private final PrintWriter uidlBuffer; + + private boolean closed = false; + + private final AbstractCommunicationManager manager; + + private int changes = 0; + + private final Set<Object> usedResources = new HashSet<Object>(); + + private boolean customLayoutArgumentsOpen = false; + + private JsonTag tag; + + private boolean cacheEnabled = false; + + private final Set<Class<? extends ClientConnector>> usedClientConnectors = new HashSet<Class<? extends ClientConnector>>(); + + /** + * Creates a new JsonPaintTarget. + * + * @param manager + * @param outWriter + * A character-output stream. + * @param cachingRequired + * true if this is not a full repaint, i.e. caches are to be + * used. + * @throws PaintException + * if the paint operation failed. + */ + public JsonPaintTarget(AbstractCommunicationManager manager, + PrintWriter outWriter, boolean cachingRequired) + throws PaintException { + + this.manager = manager; + + // Sets the target for UIDL writing + uidlBuffer = outWriter; + + // Initialize tag-writing + mOpenTags = new Stack<String>(); + openJsonTags = new Stack<JsonTag>(); + + openPaintables = new Stack<ClientConnector>(); + openPaintableTags = new Stack<String>(); + + cacheEnabled = cachingRequired; + } + + @Override + public void startTag(String tagName) throws PaintException { + startTag(tagName, false); + } + + /** + * Prints the element start tag. + * + * <pre> + * Todo: + * Checking of input values + * + * </pre> + * + * @param tagName + * the name of the start tag. + * @throws PaintException + * if the paint operation failed. + * + */ + public void startTag(String tagName, boolean isChildNode) + throws PaintException { + // In case of null data output nothing: + if (tagName == null) { + throw new NullPointerException(); + } + + // Ensures that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + if (tag != null) { + openJsonTags.push(tag); + } + // Checks tagName and attributes here + mOpenTags.push(tagName); + + tag = new JsonTag(tagName); + + customLayoutArgumentsOpen = false; + + } + + /** + * Prints the element end tag. + * + * If the parent tag is closed before every child tag is closed an + * PaintException is raised. + * + * @param tag + * the name of the end tag. + * @throws Paintexception + * if the paint operation failed. + */ + + @Override + public void endTag(String tagName) throws PaintException { + // In case of null data output nothing: + if (tagName == null) { + throw new NullPointerException(); + } + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + if (openJsonTags.size() > 0) { + final JsonTag parent = openJsonTags.pop(); + + String lastTag = ""; + + lastTag = mOpenTags.pop(); + if (!tagName.equalsIgnoreCase(lastTag)) { + throw new PaintException("Invalid UIDL: wrong ending tag: '" + + tagName + "' expected: '" + lastTag + "'."); + } + + parent.addData(tag.getJSON()); + + tag = parent; + } else { + changes++; + uidlBuffer.print(((changes > 1) ? "," : "") + tag.getJSON()); + tag = null; + } + } + + /** + * Substitutes the XML sensitive characters with predefined XML entities. + * + * @param xml + * the String to be substituted. + * @return A new string instance where all occurrences of XML sensitive + * characters are substituted with entities. + */ + static public String escapeXML(String xml) { + if (xml == null || xml.length() <= 0) { + return ""; + } + return escapeXML(new StringBuilder(xml)).toString(); + } + + /** + * Substitutes the XML sensitive characters with predefined XML entities. + * + * @param xml + * the String to be substituted. + * @return A new StringBuilder instance where all occurrences of XML + * sensitive characters are substituted with entities. + * + */ + static StringBuilder escapeXML(StringBuilder xml) { + if (xml == null || xml.length() <= 0) { + return new StringBuilder(""); + } + + final StringBuilder result = new StringBuilder(xml.length() * 2); + + for (int i = 0; i < xml.length(); i++) { + final char c = xml.charAt(i); + final String s = toXmlChar(c); + if (s != null) { + result.append(s); + } else { + result.append(c); + } + } + return result; + } + + /** + * Escapes the given string so it can safely be used as a JSON string. + * + * @param s + * The string to escape + * @return Escaped version of the string + */ + static public String escapeJSON(String s) { + // FIXME: Move this method to another class as other classes use it + // also. + if (s == null) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + final char ch = s.charAt(i); + switch (ch) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '/': + sb.append("\\/"); + break; + default: + if (ch >= '\u0000' && ch <= '\u001F') { + final String ss = Integer.toHexString(ch); + sb.append("\\u"); + for (int k = 0; k < 4 - ss.length(); k++) { + sb.append('0'); + } + sb.append(ss.toUpperCase()); + } else { + sb.append(ch); + } + } + } + return sb.toString(); + } + + /** + * Substitutes a XML sensitive character with predefined XML entity. + * + * @param c + * the Character to be replaced with an entity. + * @return String of the entity or null if character is not to be replaced + * with an entity. + */ + private static String toXmlChar(char c) { + switch (c) { + case '&': + return "&"; // & => & + case '>': + return ">"; // > => > + case '<': + return "<"; // < => < + case '"': + return """; // " => " + case '\'': + return "'"; // ' => ' + default: + return null; + } + } + + /** + * Prints XML-escaped text. + * + * @param str + * @throws PaintException + * if the paint operation failed. + * + */ + + @Override + public void addText(String str) throws PaintException { + tag.addData("\"" + escapeJSON(str) + "\""); + } + + @Override + public void addAttribute(String name, boolean value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + (value ? "true" : "false")); + } + + @Override + public void addAttribute(String name, Resource value) throws PaintException { + if (value == null) { + throw new NullPointerException(); + } + ResourceReference reference = ResourceReference.create(value); + addAttribute(name, reference.getURL()); + } + + @Override + public void addAttribute(String name, int value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, long value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, float value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, double value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + @Override + public void addAttribute(String name, String value) throws PaintException { + // In case of null data output nothing: + if ((value == null) || (name == null)) { + throw new NullPointerException( + "Parameters must be non-null strings"); + } + + tag.addAttribute("\"" + name + "\":\"" + escapeJSON(value) + "\""); + + if (customLayoutArgumentsOpen && "template".equals(name)) { + getUsedResources().add("layouts/" + value + ".html"); + } + + if (name.equals("locale")) { + manager.requireLocale(value); + } + + } + + @Override + public void addAttribute(String name, Component value) + throws PaintException { + final String id = value.getConnectorId(); + addAttribute(name, id); + } + + @Override + public void addAttribute(String name, Map<?, ?> value) + throws PaintException { + + StringBuilder sb = new StringBuilder(); + sb.append("\""); + sb.append(name); + sb.append("\":"); + sb.append("{"); + for (Iterator<?> it = value.keySet().iterator(); it.hasNext();) { + Object key = it.next(); + Object mapValue = value.get(key); + sb.append("\""); + if (key instanceof ClientConnector) { + sb.append(((ClientConnector) key).getConnectorId()); + } else { + sb.append(escapeJSON(key.toString())); + } + sb.append("\":"); + if (mapValue instanceof Float || mapValue instanceof Integer + || mapValue instanceof Double + || mapValue instanceof Boolean + || mapValue instanceof Alignment) { + sb.append(mapValue); + } else { + sb.append("\""); + sb.append(escapeJSON(mapValue.toString())); + sb.append("\""); + } + if (it.hasNext()) { + sb.append(","); + } + } + sb.append("}"); + + tag.addAttribute(sb.toString()); + } + + @Override + public void addAttribute(String name, Object[] values) { + // In case of null data output nothing: + if ((values == null) || (name == null)) { + throw new NullPointerException( + "Parameters must be non-null strings"); + } + final StringBuilder buf = new StringBuilder(); + buf.append("\"" + name + "\":["); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + buf.append(","); + } + buf.append("\""); + buf.append(escapeJSON(values[i].toString())); + buf.append("\""); + } + buf.append("]"); + tag.addAttribute(buf.toString()); + } + + @Override + public void addVariable(VariableOwner owner, String name, String value) + throws PaintException { + tag.addVariable(new StringVariable(owner, name, escapeJSON(value))); + } + + @Override + public void addVariable(VariableOwner owner, String name, Component value) + throws PaintException { + tag.addVariable(new StringVariable(owner, name, value.getConnectorId())); + } + + @Override + public void addVariable(VariableOwner owner, String name, int value) + throws PaintException { + tag.addVariable(new IntVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, long value) + throws PaintException { + tag.addVariable(new LongVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, float value) + throws PaintException { + tag.addVariable(new FloatVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, double value) + throws PaintException { + tag.addVariable(new DoubleVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, boolean value) + throws PaintException { + tag.addVariable(new BooleanVariable(owner, name, value)); + } + + @Override + public void addVariable(VariableOwner owner, String name, String[] value) + throws PaintException { + tag.addVariable(new ArrayVariable(owner, name, value)); + } + + /** + * Adds a upload stream type variable. + * + * TODO not converted for JSON + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void addUploadStreamVariable(VariableOwner owner, String name) + throws PaintException { + startTag("uploadstream"); + addAttribute(UIDL_ARG_NAME, name); + endTag("uploadstream"); + } + + /** + * Prints the single text section. + * + * Prints full text section. The section data is escaped + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the section data to be printed. + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void addSection(String sectionTagName, String sectionData) + throws PaintException { + tag.addData("{\"" + sectionTagName + "\":\"" + escapeJSON(sectionData) + + "\"}"); + } + + /** + * Adds XML directly to UIDL. + * + * @param xml + * the Xml to be added. + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void addUIDL(String xml) throws PaintException { + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + // Make sure that the open start tag is closed before + // anything is written. + + // Escape and write what was given + if (xml != null) { + tag.addData("\"" + escapeJSON(xml) + "\""); + } + + } + + /** + * Adds XML section with namespace. + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the section data. + * @param namespace + * the namespace to be added. + * @throws PaintException + * if the paint operation failed. + * + * @see com.vaadin.terminal.PaintTarget#addXMLSection(String, String, + * String) + */ + + @Override + public void addXMLSection(String sectionTagName, String sectionData, + String namespace) throws PaintException { + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + startTag(sectionTagName); + if (namespace != null) { + addAttribute("xmlns", namespace); + } + + if (sectionData != null) { + tag.addData("\"" + escapeJSON(sectionData) + "\""); + } + endTag(sectionTagName); + } + + /** + * Gets the UIDL already printed to stream. Paint target must be closed + * before the <code>getUIDL</code> can be called. + * + * @return the UIDL. + */ + public String getUIDL() { + if (closed) { + return uidlBuffer.toString(); + } + throw new IllegalStateException( + "Tried to read UIDL from open PaintTarget"); + } + + /** + * Closes the paint target. Paint target must be closed before the + * <code>getUIDL</code> can be called. Subsequent attempts to write to paint + * target. If the target was already closed, call to this function is + * ignored. will generate an exception. + * + * @throws PaintException + * if the paint operation failed. + */ + public void close() throws PaintException { + if (tag != null) { + uidlBuffer.write(tag.getJSON()); + } + flush(); + closed = true; + } + + /** + * Method flush. + */ + private void flush() { + uidlBuffer.flush(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.PaintTarget#startPaintable(com.vaadin.terminal + * .Paintable, java.lang.String) + */ + + @Override + public PaintStatus startPaintable(Component connector, String tagName) + throws PaintException { + boolean topLevelPaintable = openPaintables.isEmpty(); + + getLogger().fine( + "startPaintable for " + connector.getClass().getName() + "@" + + Integer.toHexString(connector.hashCode())); + startTag(tagName, true); + + openPaintables.push(connector); + openPaintableTags.push(tagName); + + addAttribute("id", connector.getConnectorId()); + + // Only paint top level paintables. All sub paintables are marked as + // queued and painted separately later. + if (!topLevelPaintable) { + return PaintStatus.CACHED; + } + + if (connector instanceof CustomLayout) { + customLayoutArgumentsOpen = true; + } + return PaintStatus.PAINTING; + } + + @Override + public void endPaintable(Component paintable) throws PaintException { + getLogger().fine( + "endPaintable for " + paintable.getClass().getName() + "@" + + Integer.toHexString(paintable.hashCode())); + + ClientConnector openPaintable = openPaintables.peek(); + if (paintable != openPaintable) { + throw new PaintException("Invalid UIDL: closing wrong paintable: '" + + paintable.getConnectorId() + "' expected: '" + + openPaintable.getConnectorId() + "'."); + } + // remove paintable from the stack + openPaintables.pop(); + String openTag = openPaintableTags.pop(); + endTag(openTag); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.PaintTarget#addCharacterData(java.lang.String ) + */ + + @Override + public void addCharacterData(String text) throws PaintException { + if (text != null) { + tag.addData(text); + } + } + + /** + * This is basically a container for UI components variables, that will be + * added at the end of JSON object. + * + * @author mattitahvonen + * + */ + class JsonTag implements Serializable { + boolean firstField = false; + + Vector<Object> variables = new Vector<Object>(); + + Vector<Object> children = new Vector<Object>(); + + Vector<Object> attr = new Vector<Object>(); + + StringBuilder data = new StringBuilder(); + + public boolean childrenArrayOpen = false; + + private boolean childNode = false; + + private boolean tagClosed = false; + + public JsonTag(String tagName) { + data.append("[\"" + tagName + "\""); + } + + private void closeTag() { + if (!tagClosed) { + data.append(attributesAsJsonObject()); + data.append(getData()); + // Writes the end (closing) tag + data.append("]"); + tagClosed = true; + } + } + + public String getJSON() { + if (!tagClosed) { + closeTag(); + } + return data.toString(); + } + + public void openChildrenArray() { + if (!childrenArrayOpen) { + // append("c : ["); + childrenArrayOpen = true; + // firstField = true; + } + } + + public void closeChildrenArray() { + // append("]"); + // firstField = false; + } + + public void setChildNode(boolean b) { + childNode = b; + } + + public boolean isChildNode() { + return childNode; + } + + public String startField() { + if (firstField) { + firstField = false; + return ""; + } else { + return ","; + } + } + + /** + * + * @param s + * json string, object or array + */ + public void addData(String s) { + children.add(s); + } + + public String getData() { + final StringBuilder buf = new StringBuilder(); + final Iterator<Object> it = children.iterator(); + while (it.hasNext()) { + buf.append(startField()); + buf.append(it.next()); + } + return buf.toString(); + } + + public void addAttribute(String jsonNode) { + attr.add(jsonNode); + } + + private String attributesAsJsonObject() { + final StringBuilder buf = new StringBuilder(); + buf.append(startField()); + buf.append("{"); + for (final Iterator<Object> iter = attr.iterator(); iter.hasNext();) { + final String element = (String) iter.next(); + buf.append(element); + if (iter.hasNext()) { + buf.append(","); + } + } + buf.append(tag.variablesAsJsonObject()); + buf.append("}"); + return buf.toString(); + } + + public void addVariable(Variable v) { + variables.add(v); + } + + private String variablesAsJsonObject() { + if (variables.size() == 0) { + return ""; + } + final StringBuilder buf = new StringBuilder(); + buf.append(startField()); + buf.append("\"v\":{"); + final Iterator<Object> iter = variables.iterator(); + while (iter.hasNext()) { + final Variable element = (Variable) iter.next(); + buf.append(element.getJsonPresentation()); + if (iter.hasNext()) { + buf.append(","); + } + } + buf.append("}"); + return buf.toString(); + } + } + + abstract class Variable implements Serializable { + + String name; + + public abstract String getJsonPresentation(); + } + + class BooleanVariable extends Variable implements Serializable { + boolean value; + + public BooleanVariable(VariableOwner owner, String name, boolean v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + (value == true ? "true" : "false"); + } + + } + + class StringVariable extends Variable implements Serializable { + String value; + + public StringVariable(VariableOwner owner, String name, String v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":\"" + value + "\""; + } + + } + + class IntVariable extends Variable implements Serializable { + int value; + + public IntVariable(VariableOwner owner, String name, int v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class LongVariable extends Variable implements Serializable { + long value; + + public LongVariable(VariableOwner owner, String name, long v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class FloatVariable extends Variable implements Serializable { + float value; + + public FloatVariable(VariableOwner owner, String name, float v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class DoubleVariable extends Variable implements Serializable { + double value; + + public DoubleVariable(VariableOwner owner, String name, double v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class ArrayVariable extends Variable implements Serializable { + String[] value; + + public ArrayVariable(VariableOwner owner, String name, String[] v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + StringBuilder sb = new StringBuilder(); + sb.append("\""); + sb.append(name); + sb.append("\":["); + for (int i = 0; i < value.length;) { + sb.append("\""); + sb.append(escapeJSON(value[i])); + sb.append("\""); + i++; + if (i < value.length) { + sb.append(","); + } + } + sb.append("]"); + return sb.toString(); + } + } + + public Set<Object> getUsedResources() { + return usedResources; + } + + @Override + @SuppressWarnings("unchecked") + public String getTag(ClientConnector clientConnector) { + Class<? extends ClientConnector> clientConnectorClass = clientConnector + .getClass(); + while (clientConnectorClass.isAnonymousClass()) { + clientConnectorClass = (Class<? extends ClientConnector>) clientConnectorClass + .getSuperclass(); + } + Class<?> clazz = clientConnectorClass; + while (!usedClientConnectors.contains(clazz) + && clazz.getSuperclass() != null + && ClientConnector.class.isAssignableFrom(clazz)) { + usedClientConnectors.add((Class<? extends ClientConnector>) clazz); + clazz = clazz.getSuperclass(); + } + return manager.getTagForType(clientConnectorClass); + } + + Collection<Class<? extends ClientConnector>> getUsedClientConnectors() { + return usedClientConnectors; + } + + @Override + public void addVariable(VariableOwner owner, String name, + StreamVariable value) throws PaintException { + String url = manager.getStreamVariableTargetUrl( + (ClientConnector) owner, name, value); + if (url != null) { + addVariable(owner, name, url); + } // else { //NOP this was just a cleanup by component } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.PaintTarget#isFullRepaint() + */ + + @Override + public boolean isFullRepaint() { + return !cacheEnabled; + } + + private static final Logger getLogger() { + return Logger.getLogger(JsonPaintTarget.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java b/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java new file mode 100644 index 0000000000..9dba05d2c1 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public class LegacyChangeVariablesInvocation extends MethodInvocation { + private Map<String, Object> variableChanges = new HashMap<String, Object>(); + + public LegacyChangeVariablesInvocation(String connectorId, + String variableName, Object value) { + super(connectorId, ApplicationConnection.UPDATE_VARIABLE_INTERFACE, + ApplicationConnection.UPDATE_VARIABLE_METHOD); + setVariableChange(variableName, value); + } + + public static boolean isLegacyVariableChange(String interfaceName, + String methodName) { + return ApplicationConnection.UPDATE_VARIABLE_METHOD + .equals(interfaceName) + && ApplicationConnection.UPDATE_VARIABLE_METHOD + .equals(methodName); + } + + public void setVariableChange(String name, Object value) { + variableChanges.put(name, value); + } + + public Map<String, Object> getVariableChanges() { + return variableChanges; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java b/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java new file mode 100644 index 0000000000..70c3add858 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class NoInputStreamException extends Exception { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java b/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java new file mode 100644 index 0000000000..e4db8453b0 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class NoOutputStreamException extends Exception { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java new file mode 100644 index 0000000000..70505ab5f9 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java @@ -0,0 +1,398 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.File; +import java.io.Serializable; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; +import javax.portlet.MimeResponse; +import javax.portlet.PortletConfig; +import javax.portlet.PortletMode; +import javax.portlet.PortletModeException; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; +import javax.portlet.PortletURL; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; +import javax.portlet.StateAwareResponse; +import javax.servlet.http.HttpSessionBindingListener; +import javax.xml.namespace.QName; + +import com.vaadin.Application; +import com.vaadin.terminal.ExternalResource; +import com.vaadin.ui.Root; + +/** + * TODO Write documentation, fix JavaDoc tags. + * + * This is automatically registered as a {@link HttpSessionBindingListener} when + * {@link PortletSession#setAttribute()} is called with the context as value. + * + * @author peholmst + */ +@SuppressWarnings("serial") +public class PortletApplicationContext2 extends AbstractWebApplicationContext { + + protected Map<Application, Set<PortletListener>> portletListeners = new HashMap<Application, Set<PortletListener>>(); + + protected transient PortletSession session; + protected transient PortletConfig portletConfig; + + protected HashMap<String, Application> portletWindowIdToApplicationMap = new HashMap<String, Application>(); + + private transient PortletResponse response; + + private final Map<String, QName> eventActionDestinationMap = new HashMap<String, QName>(); + private final Map<String, Serializable> eventActionValueMap = new HashMap<String, Serializable>(); + + private final Map<String, String> sharedParameterActionNameMap = new HashMap<String, String>(); + private final Map<String, String> sharedParameterActionValueMap = new HashMap<String, String>(); + + @Override + public File getBaseDirectory() { + String resultPath = session.getPortletContext().getRealPath("/"); + if (resultPath != null) { + return new File(resultPath); + } else { + try { + final URL url = session.getPortletContext().getResource("/"); + return new File(url.getFile()); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger() + .log(Level.INFO, + "Cannot access base directory, possible security issue " + + "with Application Server or Servlet Container", + e); + } + } + return null; + } + + protected PortletCommunicationManager getApplicationManager( + Application application) { + PortletCommunicationManager mgr = (PortletCommunicationManager) applicationToAjaxAppMgrMap + .get(application); + + if (mgr == null) { + // Creates a new manager + mgr = createPortletCommunicationManager(application); + applicationToAjaxAppMgrMap.put(application, mgr); + } + return mgr; + } + + protected PortletCommunicationManager createPortletCommunicationManager( + Application application) { + return new PortletCommunicationManager(application); + } + + public static PortletApplicationContext2 getApplicationContext( + PortletSession session) { + Object cxattr = session.getAttribute(PortletApplicationContext2.class + .getName()); + PortletApplicationContext2 cx = null; + // can be false also e.g. if old context comes from another + // classloader when using + // <private-session-attributes>false</private-session-attributes> + // and redeploying the portlet - see #7461 + if (cxattr instanceof PortletApplicationContext2) { + cx = (PortletApplicationContext2) cxattr; + } + if (cx == null) { + cx = new PortletApplicationContext2(); + session.setAttribute(PortletApplicationContext2.class.getName(), cx); + } + if (cx.session == null) { + cx.session = session; + } + return cx; + } + + @Override + protected void removeApplication(Application application) { + super.removeApplication(application); + // values() is backed by map, removes the key-value pair from the map + portletWindowIdToApplicationMap.values().remove(application); + } + + protected void addApplication(Application application, + String portletWindowId) { + applications.add(application); + portletWindowIdToApplicationMap.put(portletWindowId, application); + } + + public Application getApplicationForWindowId(String portletWindowId) { + return portletWindowIdToApplicationMap.get(portletWindowId); + } + + public PortletSession getPortletSession() { + return session; + } + + public PortletConfig getPortletConfig() { + return portletConfig; + } + + public void setPortletConfig(PortletConfig config) { + portletConfig = config; + } + + public void addPortletListener(Application app, PortletListener listener) { + Set<PortletListener> l = portletListeners.get(app); + if (l == null) { + l = new LinkedHashSet<PortletListener>(); + portletListeners.put(app, l); + } + l.add(listener); + } + + public void removePortletListener(Application app, PortletListener listener) { + Set<PortletListener> l = portletListeners.get(app); + if (l != null) { + l.remove(listener); + } + } + + public void firePortletRenderRequest(Application app, Root root, + RenderRequest request, RenderResponse response) { + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleRenderRequest(request, new RestrictedRenderResponse( + response), root); + } + } + } + + public void firePortletActionRequest(Application app, Root root, + ActionRequest request, ActionResponse response) { + String key = request.getParameter(ActionRequest.ACTION_NAME); + if (eventActionDestinationMap.containsKey(key)) { + // this action request is only to send queued portlet events + response.setEvent(eventActionDestinationMap.get(key), + eventActionValueMap.get(key)); + // cleanup + eventActionDestinationMap.remove(key); + eventActionValueMap.remove(key); + } else if (sharedParameterActionNameMap.containsKey(key)) { + // this action request is only to set shared render parameters + response.setRenderParameter(sharedParameterActionNameMap.get(key), + sharedParameterActionValueMap.get(key)); + // cleanup + sharedParameterActionNameMap.remove(key); + sharedParameterActionValueMap.remove(key); + } else { + // normal action request, notify listeners + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleActionRequest(request, response, root); + } + } + } + } + + public void firePortletEventRequest(Application app, Root root, + EventRequest request, EventResponse response) { + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleEventRequest(request, response, root); + } + } + } + + public void firePortletResourceRequest(Application app, Root root, + ResourceRequest request, ResourceResponse response) { + Set<PortletListener> listeners = portletListeners.get(app); + if (listeners != null) { + for (PortletListener l : listeners) { + l.handleResourceRequest(request, response, root); + } + } + } + + public interface PortletListener extends Serializable { + + public void handleRenderRequest(RenderRequest request, + RenderResponse response, Root root); + + public void handleActionRequest(ActionRequest request, + ActionResponse response, Root root); + + public void handleEventRequest(EventRequest request, + EventResponse response, Root root); + + public void handleResourceRequest(ResourceRequest request, + ResourceResponse response, Root root); + } + + /** + * This is for use by {@link AbstractApplicationPortlet} only. + * + * TODO cleaner implementation, now "semi-static"! + * + * @param mimeResponse + */ + void setResponse(PortletResponse response) { + this.response = response; + } + + /** + * Creates a new action URL. + * + * @param action + * @return action URL or null if called outside a MimeRequest (outside a + * UIDL request or similar) + */ + public PortletURL generateActionURL(String action) { + PortletURL url = null; + if (response instanceof MimeResponse) { + url = ((MimeResponse) response).createActionURL(); + url.setParameter("javax.portlet.action", action); + } else { + return null; + } + return url; + } + + /** + * Sends a portlet event to the indicated destination. + * + * Internally, an action may be created and opened, as an event cannot be + * sent directly from all types of requests. + * + * The event destinations and values need to be kept in the context until + * sent. Any memory leaks if the action fails are limited to the session. + * + * Event names for events sent and received by a portlet need to be declared + * in portlet.xml . + * + * @param root + * a window in which a temporary action URL can be opened if + * necessary + * @param name + * event name + * @param value + * event value object that is Serializable and, if appropriate, + * has a valid JAXB annotation + */ + public void sendPortletEvent(Root root, QName name, Serializable value) + throws IllegalStateException { + if (response instanceof MimeResponse) { + String actionKey = "" + System.currentTimeMillis(); + while (eventActionDestinationMap.containsKey(actionKey)) { + actionKey = actionKey + "."; + } + PortletURL actionUrl = generateActionURL(actionKey); + if (actionUrl != null) { + eventActionDestinationMap.put(actionKey, name); + eventActionValueMap.put(actionKey, value); + root.getPage().open(new ExternalResource(actionUrl.toString())); + } else { + // this should never happen as we already know the response is a + // MimeResponse + throw new IllegalStateException( + "Portlet events can only be sent from a portlet request"); + } + } else if (response instanceof StateAwareResponse) { + ((StateAwareResponse) response).setEvent(name, value); + } else { + throw new IllegalStateException( + "Portlet events can only be sent from a portlet request"); + } + } + + /** + * Sets a shared portlet parameter. + * + * Internally, an action may be created and opened, as shared parameters + * cannot be set directly from all types of requests. + * + * The parameters and values need to be kept in the context until sent. Any + * memory leaks if the action fails are limited to the session. + * + * Shared parameters set or read by a portlet need to be declared in + * portlet.xml . + * + * @param root + * a window in which a temporary action URL can be opened if + * necessary + * @param name + * parameter identifier + * @param value + * parameter value + */ + public void setSharedRenderParameter(Root root, String name, String value) + throws IllegalStateException { + if (response instanceof MimeResponse) { + String actionKey = "" + System.currentTimeMillis(); + while (sharedParameterActionNameMap.containsKey(actionKey)) { + actionKey = actionKey + "."; + } + PortletURL actionUrl = generateActionURL(actionKey); + if (actionUrl != null) { + sharedParameterActionNameMap.put(actionKey, name); + sharedParameterActionValueMap.put(actionKey, value); + root.getPage().open(new ExternalResource(actionUrl.toString())); + } else { + // this should never happen as we already know the response is a + // MimeResponse + throw new IllegalStateException( + "Shared parameters can only be set from a portlet request"); + } + } else if (response instanceof StateAwareResponse) { + ((StateAwareResponse) response).setRenderParameter(name, value); + } else { + throw new IllegalStateException( + "Shared parameters can only be set from a portlet request"); + } + } + + /** + * Sets the portlet mode. This may trigger a new render request. + * + * Portlet modes used by a portlet need to be declared in portlet.xml . + * + * @param root + * a window in which the render URL can be opened if necessary + * @param portletMode + * the portlet mode to switch to + * @throws PortletModeException + * if the portlet mode is not allowed for some reason + * (configuration, permissions etc.) + */ + public void setPortletMode(Root root, PortletMode portletMode) + throws IllegalStateException, PortletModeException { + if (response instanceof MimeResponse) { + PortletURL url = ((MimeResponse) response).createRenderURL(); + url.setPortletMode(portletMode); + throw new RuntimeException("Root.open has not yet been implemented"); + // root.open(new ExternalResource(url.toString())); + } else if (response instanceof StateAwareResponse) { + ((StateAwareResponse) response).setPortletMode(portletMode); + } else { + throw new IllegalStateException( + "Portlet mode can only be changed from a portlet request"); + } + } + + private Logger getLogger() { + return Logger.getLogger(PortletApplicationContext2.class.getName()); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java new file mode 100644 index 0000000000..39c27d05fe --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java @@ -0,0 +1,170 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.InputStream; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletContext; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceURL; + +import com.vaadin.Application; +import com.vaadin.external.json.JSONException; +import com.vaadin.external.json.JSONObject; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConfiguration; +import com.vaadin.ui.Root; + +/** + * TODO document me! + * + * @author peholmst + * + */ +@SuppressWarnings("serial") +public class PortletCommunicationManager extends AbstractCommunicationManager { + + public PortletCommunicationManager(Application application) { + super(application); + } + + @Override + protected BootstrapHandler createBootstrapHandler() { + return new BootstrapHandler() { + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + PortletRequest portletRequest = WrappedPortletRequest.cast( + request).getPortletRequest(); + if (portletRequest instanceof RenderRequest) { + return super.handleRequest(application, request, response); + } else { + return false; + } + } + + @Override + protected String getApplicationId(BootstrapContext context) { + PortletRequest portletRequest = WrappedPortletRequest.cast( + context.getRequest()).getPortletRequest(); + /* + * We need to generate a unique ID because some portals already + * create a DIV with the portlet's Window ID as the DOM ID. + */ + return "v-" + portletRequest.getWindowID(); + } + + @Override + protected String getAppUri(BootstrapContext context) { + return getRenderResponse(context).createActionURL().toString(); + } + + private RenderResponse getRenderResponse(BootstrapContext context) { + PortletResponse response = ((WrappedPortletResponse) context + .getResponse()).getPortletResponse(); + + RenderResponse renderResponse = (RenderResponse) response; + return renderResponse; + } + + @Override + protected JSONObject getDefaultParameters(BootstrapContext context) + throws JSONException { + /* + * We need this in order to get uploads to work. TODO this is + * not needed for uploads anymore, check if this is needed for + * some other things + */ + JSONObject defaults = super.getDefaultParameters(context); + + ResourceURL portletResourceUrl = getRenderResponse(context) + .createResourceURL(); + portletResourceUrl + .setResourceID(AbstractApplicationPortlet.RESOURCE_URL_ID); + defaults.put(ApplicationConfiguration.PORTLET_RESOUCE_URL_BASE, + portletResourceUrl.toString()); + + defaults.put("pathInfo", ""); + + return defaults; + } + + @Override + protected void appendMainScriptTagContents( + BootstrapContext context, StringBuilder builder) + throws JSONException, IOException { + // fixed base theme to use - all portal pages with Vaadin + // applications will load this exactly once + String portalTheme = WrappedPortletRequest + .cast(context.getRequest()) + .getPortalProperty( + AbstractApplicationPortlet.PORTAL_PARAMETER_VAADIN_THEME); + if (portalTheme != null + && !portalTheme.equals(context.getThemeName())) { + String portalThemeUri = getThemeUri(context, portalTheme); + // XSS safe - originates from portal properties + builder.append("vaadin.loadTheme('" + portalThemeUri + + "');"); + } + + super.appendMainScriptTagContents(context, builder); + } + + @Override + protected String getMainDivStyle(BootstrapContext context) { + DeploymentConfiguration deploymentConfiguration = context + .getRequest().getDeploymentConfiguration(); + return deploymentConfiguration.getApplicationOrSystemProperty( + AbstractApplicationPortlet.PORTLET_PARAMETER_STYLE, + null); + } + + @Override + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException, JSONException { + return PortletCommunicationManager.this.getInitialUIDL(request, + root); + } + + @Override + protected JSONObject getApplicationParameters( + BootstrapContext context) throws JSONException, + PaintException { + JSONObject parameters = super.getApplicationParameters(context); + WrappedPortletResponse wrappedPortletResponse = (WrappedPortletResponse) context + .getResponse(); + MimeResponse portletResponse = (MimeResponse) wrappedPortletResponse + .getPortletResponse(); + ResourceURL resourceURL = portletResponse.createResourceURL(); + resourceURL.setResourceID("browserDetails"); + parameters.put("browserDetailsUrl", resourceURL.toString()); + return parameters; + } + + }; + + } + + @Override + protected InputStream getThemeResourceAsStream(Root root, String themeName, + String resource) { + PortletApplicationContext2 context = (PortletApplicationContext2) root + .getApplication().getContext(); + PortletContext portletContext = context.getPortletSession() + .getPortletContext(); + return portletContext.getResourceAsStream("/" + + AbstractApplicationPortlet.THEME_DIRECTORY_PATH + themeName + + "/" + resource); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java b/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java new file mode 100644 index 0000000000..8a30f5c1d4 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java @@ -0,0 +1,56 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.servlet.Filter; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext.TransactionListener; +import com.vaadin.terminal.Terminal; + +/** + * An {@link Application} that implements this interface gets notified of + * request start and end by the terminal. It is quite similar to the + * {@link HttpServletRequestListener}, but the parameters are Portlet specific. + * If an Application is deployed as both a Servlet and a Portlet, one most + * likely needs to implement both. + * <p> + * Only JSR 286 style Portlets are supported. + * <p> + * The interface can be used for several helper tasks including: + * <ul> + * <li>Opening and closing database connections + * <li>Implementing {@link ThreadLocal} + * <li>Inter-portlet communication + * </ul> + * <p> + * Alternatives for implementing similar features are are Servlet {@link Filter} + * s and {@link TransactionListener}s in Vaadin. + * + * @since 6.2 + * @see HttpServletRequestListener + */ +public interface PortletRequestListener extends Serializable { + + /** + * This method is called before {@link Terminal} applies the request to + * Application. + * + * @param requestData + * the {@link PortletRequest} about to change Application state + */ + public void onRequestStart(PortletRequest request, PortletResponse response); + + /** + * This method is called at the end of each request. + * + * @param requestData + * the {@link PortletRequest} + */ + public void onRequestEnd(PortletRequest request, PortletResponse response); +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java b/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java new file mode 100644 index 0000000000..6c0edec466 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java @@ -0,0 +1,43 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +/** + * Times the handling of requests and stores the information as an attribute in + * the request. The timing info is later passed on to the client in the UIDL and + * the client provides JavaScript API for accessing this data from e.g. + * TestBench. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public class RequestTimer implements Serializable { + private long requestStartTime = 0; + + /** + * Starts the timing of a request. This should be called before any + * processing of the request. + */ + public void start() { + requestStartTime = System.nanoTime(); + } + + /** + * Stops the timing of a request. This should be called when all processing + * of a request has finished. + * + * @param context + */ + public void stop(AbstractWebApplicationContext context) { + // Measure and store the total handling time. This data can be + // used in TestBench 3 tests. + long time = (System.nanoTime() - requestStartTime) / 1000000; + + // The timings must be stored in the context, since a new + // RequestTimer is created for every request. + context.setLastRequestTime(time); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java b/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java new file mode 100644 index 0000000000..2104ad4b87 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java @@ -0,0 +1,67 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.Application; +import com.vaadin.shared.communication.URLReference; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.ExternalResource; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.ThemeResource; + +public class ResourceReference extends URLReference { + + private Resource resource; + + public ResourceReference(Resource resource) { + this.resource = resource; + } + + public Resource getResource() { + return resource; + } + + @Override + public String getURL() { + if (resource instanceof ExternalResource) { + return ((ExternalResource) resource).getURL(); + } else if (resource instanceof ApplicationResource) { + final ApplicationResource r = (ApplicationResource) resource; + final Application a = r.getApplication(); + if (a == null) { + throw new RuntimeException( + "An ApplicationResource (" + + r.getClass().getName() + + " must be attached to an application when it is sent to the client."); + } + final String uri = a.getRelativeLocation(r); + return uri; + } else if (resource instanceof ThemeResource) { + final String uri = "theme://" + + ((ThemeResource) resource).getResourceId(); + return uri; + } else { + throw new RuntimeException(getClass().getSimpleName() + + " does not support resources of type: " + + resource.getClass().getName()); + } + + } + + public static ResourceReference create(Resource resource) { + if (resource == null) { + return null; + } else { + return new ResourceReference(resource); + } + } + + public static Resource getResource(URLReference reference) { + if (reference == null) { + return null; + } + assert reference instanceof ResourceReference; + return ((ResourceReference) reference).getResource(); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java b/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java new file mode 100644 index 0000000000..9fdffbf9a5 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java @@ -0,0 +1,172 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.Collection; +import java.util.Locale; + +import javax.portlet.CacheControl; +import javax.portlet.PortletMode; +import javax.portlet.PortletURL; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceURL; +import javax.servlet.http.Cookie; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; + +/** + * Read-only wrapper for a {@link RenderResponse}. + * + * Only for use by {@link PortletApplicationContext} and + * {@link PortletApplicationContext2}. + */ +class RestrictedRenderResponse implements RenderResponse, Serializable { + + private RenderResponse response; + + RestrictedRenderResponse(RenderResponse response) { + this.response = response; + } + + @Override + public void addProperty(String key, String value) { + response.addProperty(key, value); + } + + @Override + public PortletURL createActionURL() { + return response.createActionURL(); + } + + @Override + public PortletURL createRenderURL() { + return response.createRenderURL(); + } + + @Override + public String encodeURL(String path) { + return response.encodeURL(path); + } + + @Override + public void flushBuffer() throws IOException { + // NOP + // TODO throw? + } + + @Override + public int getBufferSize() { + return response.getBufferSize(); + } + + @Override + public String getCharacterEncoding() { + return response.getCharacterEncoding(); + } + + @Override + public String getContentType() { + return response.getContentType(); + } + + @Override + public Locale getLocale() { + return response.getLocale(); + } + + @Override + public String getNamespace() { + return response.getNamespace(); + } + + @Override + public OutputStream getPortletOutputStream() throws IOException { + // write forbidden + return null; + } + + @Override + public PrintWriter getWriter() throws IOException { + // write forbidden + return null; + } + + @Override + public boolean isCommitted() { + return response.isCommitted(); + } + + @Override + public void reset() { + // NOP + // TODO throw? + } + + @Override + public void resetBuffer() { + // NOP + // TODO throw? + } + + @Override + public void setBufferSize(int size) { + // NOP + // TODO throw? + } + + @Override + public void setContentType(String type) { + // NOP + // TODO throw? + } + + @Override + public void setProperty(String key, String value) { + response.setProperty(key, value); + } + + @Override + public void setTitle(String title) { + response.setTitle(title); + } + + @Override + public void setNextPossiblePortletModes(Collection<PortletMode> portletModes) { + // NOP + // TODO throw? + } + + @Override + public ResourceURL createResourceURL() { + return response.createResourceURL(); + } + + @Override + public CacheControl getCacheControl() { + return response.getCacheControl(); + } + + @Override + public void addProperty(Cookie cookie) { + // NOP + // TODO throw? + } + + @Override + public void addProperty(String key, Element element) { + // NOP + // TODO throw? + } + + @Override + public Element createElement(String tagName) throws DOMException { + // NOP + return null; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/RpcManager.java b/server/src/com/vaadin/terminal/gwt/server/RpcManager.java new file mode 100644 index 0000000000..026c847e2b --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RpcManager.java @@ -0,0 +1,48 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +/** + * Server side RPC manager that can invoke methods based on RPC calls received + * from the client. + * + * @since 7.0 + */ +public interface RpcManager extends Serializable { + public void applyInvocation(ServerRpcMethodInvocation invocation) + throws RpcInvocationException; + + /** + * Wrapper exception for exceptions which occur during invocation of an RPC + * call + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0 + * + */ + public static class RpcInvocationException extends Exception { + + public RpcInvocationException() { + super(); + } + + public RpcInvocationException(String message, Throwable cause) { + super(message, cause); + } + + public RpcInvocationException(String message) { + super(message); + } + + public RpcInvocationException(Throwable cause) { + super(cause); + } + + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java b/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java new file mode 100644 index 0000000000..b280f5c6b5 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import com.vaadin.terminal.VariableOwner; + +/** + * Marker interface for server side classes that can receive RPC calls. + * + * This plays a role similar to that of {@link VariableOwner}. + * + * @since 7.0 + */ +public interface RpcTarget extends Serializable { + /** + * Returns the RPC manager instance to use when receiving calls for an RPC + * interface. + * + * @param rpcInterface + * interface for which the call was made + * @return RpcManager or null if none found for the interface + */ + public RpcManager getRpcManager(Class<?> rpcInterface); +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java b/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java new file mode 100644 index 0000000000..1c7af82a36 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java @@ -0,0 +1,142 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.shared.Connector; + +/** + * Server side RPC manager that handles RPC calls coming from the client. + * + * Each {@link RpcTarget} (typically a {@link ClientConnector}) should have its + * own instance of {@link ServerRpcManager} if it wants to receive RPC calls + * from the client. + * + * @since 7.0 + */ +public class ServerRpcManager<T> implements RpcManager { + + private final T implementation; + private final Class<T> rpcInterface; + + private static final Map<Class<?>, Class<?>> boxedTypes = new HashMap<Class<?>, Class<?>>(); + static { + try { + Class<?>[] boxClasses = new Class<?>[] { Boolean.class, Byte.class, + Short.class, Character.class, Integer.class, Long.class, + Float.class, Double.class }; + for (Class<?> boxClass : boxClasses) { + Field typeField = boxClass.getField("TYPE"); + Class<?> primitiveType = (Class<?>) typeField.get(boxClass); + boxedTypes.put(primitiveType, boxClass); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Create a RPC manager for an RPC target. + * + * @param target + * RPC call target (normally a {@link Connector}) + * @param implementation + * RPC interface implementation for the target + * @param rpcInterface + * RPC interface type + */ + public ServerRpcManager(T implementation, Class<T> rpcInterface) { + this.implementation = implementation; + this.rpcInterface = rpcInterface; + } + + /** + * Invoke a method in a server side RPC target class. This method is to be + * used by the RPC framework and unit testing tools only. + * + * @param target + * non-null target of the RPC call + * @param invocation + * method invocation to perform + * @throws RpcInvocationException + */ + public static void applyInvocation(RpcTarget target, + ServerRpcMethodInvocation invocation) throws RpcInvocationException { + RpcManager manager = target.getRpcManager(invocation + .getInterfaceClass()); + if (manager != null) { + manager.applyInvocation(invocation); + } else { + getLogger() + .log(Level.WARNING, + "RPC call received for RpcTarget " + + target.getClass().getName() + + " (" + + invocation.getConnectorId() + + ") but the target has not registered any RPC interfaces"); + } + } + + /** + * Returns the RPC interface implementation for the RPC target. + * + * @return RPC interface implementation + */ + protected T getImplementation() { + return implementation; + } + + /** + * Returns the RPC interface type managed by this RPC manager instance. + * + * @return RPC interface type + */ + protected Class<T> getRpcInterface() { + return rpcInterface; + } + + /** + * Invoke a method in a server side RPC target class. This method is to be + * used by the RPC framework and unit testing tools only. + * + * @param invocation + * method invocation to perform + */ + @Override + public void applyInvocation(ServerRpcMethodInvocation invocation) + throws RpcInvocationException { + Method method = invocation.getMethod(); + Class<?>[] parameterTypes = method.getParameterTypes(); + Object[] args = new Object[parameterTypes.length]; + Object[] arguments = invocation.getParameters(); + for (int i = 0; i < args.length; i++) { + // no conversion needed for basic cases + // Class<?> type = parameterTypes[i]; + // if (type.isPrimitive()) { + // type = boxedTypes.get(type); + // } + args[i] = arguments[i]; + } + try { + method.invoke(implementation, args); + } catch (Exception e) { + throw new RpcInvocationException("Unable to invoke method " + + invocation.getMethodName() + " in " + + invocation.getInterfaceName(), e); + } + } + + private static Logger getLogger() { + return Logger.getLogger(ServerRpcManager.class.getName()); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java b/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java new file mode 100644 index 0000000000..ff81a27596 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java @@ -0,0 +1,113 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.ServerRpc; + +public class ServerRpcMethodInvocation extends MethodInvocation { + + private static final Map<String, Method> invocationMethodCache = new ConcurrentHashMap<String, Method>( + 128, 0.75f, 1); + + private final Method method; + + private Class<? extends ServerRpc> interfaceClass; + + public ServerRpcMethodInvocation(String connectorId, String interfaceName, + String methodName, int parameterCount) { + super(connectorId, interfaceName, methodName); + + interfaceClass = findClass(); + method = findInvocationMethod(interfaceClass, methodName, + parameterCount); + } + + private Class<? extends ServerRpc> findClass() { + try { + Class<?> rpcInterface = Class.forName(getInterfaceName()); + if (!ServerRpc.class.isAssignableFrom(rpcInterface)) { + throw new IllegalArgumentException("The interface " + + getInterfaceName() + "is not a server RPC interface."); + } + return (Class<? extends ServerRpc>) rpcInterface; + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("The server RPC interface " + + getInterfaceName() + " could not be found", e); + } finally { + + } + } + + public Class<? extends ServerRpc> getInterfaceClass() { + return interfaceClass; + } + + public Method getMethod() { + return method; + } + + /** + * Tries to find the method from the cache or alternatively by invoking + * {@link #doFindInvocationMethod(Class, String, int)} and updating the + * cache. + * + * @param targetType + * @param methodName + * @param parameterCount + * @return + */ + private Method findInvocationMethod(Class<?> targetType, String methodName, + int parameterCount) { + // TODO currently only using method name and number of parameters as the + // signature + String signature = targetType.getName() + "." + methodName + "(" + + parameterCount; + Method invocationMethod = invocationMethodCache.get(signature); + + if (invocationMethod == null) { + invocationMethod = doFindInvocationMethod(targetType, methodName, + parameterCount); + + if (invocationMethod != null) { + invocationMethodCache.put(signature, invocationMethod); + } + } + + if (invocationMethod == null) { + throw new IllegalStateException("Can't find method " + methodName + + " with " + parameterCount + " parameters in " + + targetType.getName()); + } + + return invocationMethod; + } + + /** + * Tries to find the method from the class by looping through available + * methods. + * + * @param targetType + * @param methodName + * @param parameterCount + * @return + */ + private Method doFindInvocationMethod(Class<?> targetType, + String methodName, int parameterCount) { + Method[] methods = targetType.getMethods(); + for (Method method : methods) { + Class<?>[] parameterTypes = method.getParameterTypes(); + if (method.getName().equals(methodName) + && parameterTypes.length == parameterCount) { + return method; + } + } + return null; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java new file mode 100644 index 0000000000..2a1dc31897 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java @@ -0,0 +1,120 @@ +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.Root; + +/* + @VaadinApache2LicenseForJavaFiles@ + */ + +class ServletPortletHelper implements Serializable { + public static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/"; + + public static class ApplicationClassException extends Exception { + + public ApplicationClassException(String message, Throwable cause) { + super(message, cause); + } + + public ApplicationClassException(String message) { + super(message); + } + } + + static Class<? extends Application> getApplicationClass( + DeploymentConfiguration deploymentConfiguration) + throws ApplicationClassException { + String applicationParameter = deploymentConfiguration + .getInitParameters().getProperty("application"); + String rootParameter = deploymentConfiguration.getInitParameters() + .getProperty(Application.ROOT_PARAMETER); + ClassLoader classLoader = deploymentConfiguration.getClassLoader(); + + if (applicationParameter == null) { + + // Validate the parameter value + verifyRootClass(rootParameter, classLoader); + + // Application can be used if a valid rootLayout is defined + return Application.class; + } + + try { + return (Class<? extends Application>) classLoader + .loadClass(applicationParameter); + } catch (final ClassNotFoundException e) { + throw new ApplicationClassException( + "Failed to load application class: " + applicationParameter, + e); + } + } + + private static void verifyRootClass(String className, + ClassLoader classLoader) throws ApplicationClassException { + if (className == null) { + throw new ApplicationClassException(Application.ROOT_PARAMETER + + " init parameter not defined"); + } + + // Check that the root layout class can be found + try { + Class<?> rootClass = classLoader.loadClass(className); + if (!Root.class.isAssignableFrom(rootClass)) { + throw new ApplicationClassException(className + + " does not implement Root"); + } + // Try finding a default constructor, else throw exception + rootClass.getConstructor(); + } catch (ClassNotFoundException e) { + throw new ApplicationClassException(className + + " could not be loaded", e); + } catch (SecurityException e) { + throw new ApplicationClassException("Could not access " + className + + " class", e); + } catch (NoSuchMethodException e) { + throw new ApplicationClassException(className + + " doesn't have a public no-args constructor"); + } + } + + private static boolean hasPathPrefix(WrappedRequest request, String prefix) { + String pathInfo = request.getRequestPathInfo(); + + if (pathInfo == null) { + return false; + } + + if (!prefix.startsWith("/")) { + prefix = '/' + prefix; + } + + if (pathInfo.startsWith(prefix)) { + return true; + } + + return false; + } + + public static boolean isFileUploadRequest(WrappedRequest request) { + return hasPathPrefix(request, UPLOAD_URL_PREFIX); + } + + public static boolean isConnectorResourceRequest(WrappedRequest request) { + return hasPathPrefix(request, + ApplicationConnection.CONNECTOR_RESOURCE_PREFIX + "/"); + } + + public static boolean isUIDLRequest(WrappedRequest request) { + return hasPathPrefix(request, ApplicationConnection.UIDL_REQUEST_PATH); + } + + public static boolean isApplicationResourceRequest(WrappedRequest request) { + return hasPathPrefix(request, ApplicationConnection.APP_REQUEST_PATH); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java b/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java new file mode 100644 index 0000000000..37b76de443 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java @@ -0,0 +1,9 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class SessionExpiredException extends Exception { + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java new file mode 100644 index 0000000000..0d4963bd7d --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java @@ -0,0 +1,16 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingEndEvent; + +@SuppressWarnings("serial") +final class StreamingEndEventImpl extends AbstractStreamingEvent implements + StreamingEndEvent { + + public StreamingEndEventImpl(String filename, String type, long totalBytes) { + super(filename, type, totalBytes, totalBytes); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java new file mode 100644 index 0000000000..6ab3df2789 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; + +@SuppressWarnings("serial") +final class StreamingErrorEventImpl extends AbstractStreamingEvent implements + StreamingErrorEvent { + + private final Exception exception; + + public StreamingErrorEventImpl(final String filename, final String type, + long contentLength, long bytesReceived, final Exception exception) { + super(filename, type, contentLength, bytesReceived); + this.exception = exception; + } + + @Override + public final Exception getException() { + return exception; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java new file mode 100644 index 0000000000..cfa7a1b98d --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java @@ -0,0 +1,17 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingProgressEvent; + +@SuppressWarnings("serial") +final class StreamingProgressEventImpl extends AbstractStreamingEvent implements + StreamingProgressEvent { + + public StreamingProgressEventImpl(final String filename, final String type, + long contentLength, long bytesReceived) { + super(filename, type, contentLength, bytesReceived); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java new file mode 100644 index 0000000000..274d05e111 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java @@ -0,0 +1,28 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import com.vaadin.terminal.StreamVariable.StreamingStartEvent; + +@SuppressWarnings("serial") +final class StreamingStartEventImpl extends AbstractStreamingEvent implements + StreamingStartEvent { + + private boolean disposed; + + public StreamingStartEventImpl(final String filename, final String type, + long contentLength) { + super(filename, type, contentLength, 0); + } + + @Override + public void disposeStreamVariable() { + disposed = true; + } + + boolean isDisposed() { + return disposed; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java b/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java new file mode 100644 index 0000000000..d15ff8a7ef --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class SystemMessageException extends RuntimeException { + + /** + * Cause of the method exception + */ + private Throwable cause; + + /** + * Constructs a new <code>SystemMessageException</code> with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public SystemMessageException(String msg) { + super(msg); + } + + /** + * Constructs a new <code>SystemMessageException</code> with the specified + * detail message and cause. + * + * @param msg + * the detail message. + * @param cause + * the cause of the exception. + */ + public SystemMessageException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructs a new <code>SystemMessageException</code> from another + * exception. + * + * @param cause + * the cause of the exception. + */ + public SystemMessageException(Throwable cause) { + this.cause = cause; + } + + /** + * @see java.lang.Throwable#getCause() + */ + @Override + public Throwable getCause() { + return cause; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java b/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java new file mode 100644 index 0000000000..5248af595e --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java @@ -0,0 +1,89 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.Writer; + +import com.vaadin.Application; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; + +/** + * A {@link RequestHandler} that presents an informative page if the browser in + * use is unsupported. Recognizes Chrome Frame and allow it to be used. + * + * <p> + * This handler is usually added to the application by + * {@link AbstractCommunicationManager}. + * </p> + */ +@SuppressWarnings("serial") +public class UnsupportedBrowserHandler implements RequestHandler { + + /** Cookie used to ignore browser checks */ + public static final String FORCE_LOAD_COOKIE = "vaadinforceload=1"; + + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + + if (request.getBrowserDetails() != null) { + // Check if the browser is supported + // If Chrome Frame is available we'll assume it's ok + WebBrowser b = request.getBrowserDetails().getWebBrowser(); + if (b.isTooOldToFunctionProperly() && !b.isChromeFrameCapable()) { + // bypass if cookie set + String c = request.getHeader("Cookie"); + if (c == null || !c.contains(FORCE_LOAD_COOKIE)) { + writeBrowserTooOldPage(request, response); + return true; // request handled + } + } + } + + return false; // pass to next handler + } + + /** + * Writes a page encouraging the user to upgrade to a more current browser. + * + * @param request + * @param response + * @throws IOException + */ + protected void writeBrowserTooOldPage(WrappedRequest request, + WrappedResponse response) throws IOException { + Writer page = response.getWriter(); + WebBrowser b = request.getBrowserDetails().getWebBrowser(); + + page.write("<html><body><h1>I'm sorry, but your browser is not supported</h1>" + + "<p>The version (" + + b.getBrowserMajorVersion() + + "." + + b.getBrowserMinorVersion() + + ") of the browser you are using " + + " is outdated and not supported.</p>" + + "<p>You should <b>consider upgrading</b> to a more up-to-date browser.</p> " + + "<p>The most popular browsers are <b>" + + " <a href=\"https://www.google.com/chrome\">Chrome</a>," + + " <a href=\"http://www.mozilla.com/firefox\">Firefox</a>," + + (b.isWindows() ? " <a href=\"http://windows.microsoft.com/en-US/internet-explorer/downloads/ie\">Internet Explorer</a>," + : "") + + " <a href=\"http://www.opera.com/browser\">Opera</a>" + + " and <a href=\"http://www.apple.com/safari\">Safari</a>.</b><br/>" + + "Upgrading to the latest version of one of these <b>will make the web safer, faster and better looking.</b></p>" + + (b.isIE() ? "<script type=\"text/javascript\" src=\"http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js\"></script>" + + "<p>If you can not upgrade your browser, please consider trying <a onclick=\"CFInstall.check({mode:'overlay'});return false;\" href=\"http://www.google.com/chromeframe\">Chrome Frame</a>.</p>" + : "") // + + "<p><sub><a onclick=\"document.cookie='" + + FORCE_LOAD_COOKIE + + "';window.location.reload();return false;\" href=\"#\">Continue without updating</a> (not recommended)</sub></p>" + + "</body>\n" + "</html>"); + + page.close(); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/UploadException.java b/server/src/com/vaadin/terminal/gwt/server/UploadException.java new file mode 100644 index 0000000000..58253da0fb --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/UploadException.java @@ -0,0 +1,15 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class UploadException extends Exception { + public UploadException(Exception e) { + super("Upload failed", e); + } + + public UploadException(String msg) { + super(msg); + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java new file mode 100644 index 0000000000..36c08b2ed9 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java @@ -0,0 +1,180 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.File; +import java.util.Enumeration; +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import com.vaadin.Application; + +/** + * Web application context for Vaadin applications. + * + * This is automatically added as a {@link HttpSessionBindingListener} when + * added to a {@link HttpSession}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.1 + */ +@SuppressWarnings("serial") +public class WebApplicationContext extends AbstractWebApplicationContext { + + protected transient HttpSession session; + private transient boolean reinitializingSession = false; + + /** + * Stores a reference to the currentRequest. Null it not inside a request. + */ + private transient Object currentRequest = null; + + /** + * Creates a new Web Application Context. + * + */ + protected WebApplicationContext() { + + } + + @Override + protected void startTransaction(Application application, Object request) { + currentRequest = request; + super.startTransaction(application, request); + } + + @Override + protected void endTransaction(Application application, Object request) { + super.endTransaction(application, request); + currentRequest = null; + } + + @Override + public void valueUnbound(HttpSessionBindingEvent event) { + if (!reinitializingSession) { + // Avoid closing the application if we are only reinitializing the + // session. Closing the application would cause the state to be lost + // and a new application to be created, which is not what we want. + super.valueUnbound(event); + } + } + + /** + * Discards the current session and creates a new session with the same + * contents. The purpose of this is to introduce a new session key in order + * to avoid session fixation attacks. + */ + @SuppressWarnings("unchecked") + public void reinitializeSession() { + + HttpSession oldSession = getHttpSession(); + + // Stores all attributes (security key, reference to this context + // instance) so they can be added to the new session + HashMap<String, Object> attrs = new HashMap<String, Object>(); + for (Enumeration<String> e = oldSession.getAttributeNames(); e + .hasMoreElements();) { + String name = e.nextElement(); + attrs.put(name, oldSession.getAttribute(name)); + } + + // Invalidate the current session, set flag to avoid call to + // valueUnbound + reinitializingSession = true; + oldSession.invalidate(); + reinitializingSession = false; + + // Create a new session + HttpSession newSession = ((HttpServletRequest) currentRequest) + .getSession(); + + // Restores all attributes (security key, reference to this context + // instance) + for (String name : attrs.keySet()) { + newSession.setAttribute(name, attrs.get(name)); + } + + // Update the "current session" variable + session = newSession; + } + + /** + * Gets the application context base directory. + * + * @see com.vaadin.service.ApplicationContext#getBaseDirectory() + */ + @Override + public File getBaseDirectory() { + final String realPath = ApplicationServlet.getResourcePath( + session.getServletContext(), "/"); + if (realPath == null) { + return null; + } + return new File(realPath); + } + + /** + * Gets the http-session application is running in. + * + * @return HttpSession this application context resides in. + */ + public HttpSession getHttpSession() { + return session; + } + + /** + * Gets the application context for an HttpSession. + * + * @param session + * the HTTP session. + * @return the application context for HttpSession. + */ + static public WebApplicationContext getApplicationContext( + HttpSession session) { + WebApplicationContext cx = (WebApplicationContext) session + .getAttribute(WebApplicationContext.class.getName()); + if (cx == null) { + cx = new WebApplicationContext(); + session.setAttribute(WebApplicationContext.class.getName(), cx); + } + if (cx.session == null) { + cx.session = session; + } + return cx; + } + + protected void addApplication(Application application) { + applications.add(application); + } + + /** + * Gets communication manager for an application. + * + * If this application has not been running before, a new manager is + * created. + * + * @param application + * @return CommunicationManager + */ + public CommunicationManager getApplicationManager(Application application, + AbstractApplicationServlet servlet) { + CommunicationManager mgr = (CommunicationManager) applicationToAjaxAppMgrMap + .get(application); + + if (mgr == null) { + // Creates new manager + mgr = servlet.createCommunicationManager(application); + applicationToAjaxAppMgrMap.put(application, mgr); + } + return mgr; + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java b/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java new file mode 100644 index 0000000000..4b92b12b66 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java @@ -0,0 +1,462 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.Date; +import java.util.Locale; + +import com.vaadin.shared.VBrowserDetails; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.WrappedRequest; + +/** + * Class that provides information about the web browser the user is using. + * Provides information such as browser name and version, screen resolution and + * IP address. + * + * @author Vaadin Ltd. + * @version @VERSION@ + */ +public class WebBrowser implements Terminal { + + private int screenHeight = 0; + private int screenWidth = 0; + private String browserApplication = null; + private Locale locale; + private String address; + private boolean secureConnection; + private int timezoneOffset = 0; + private int rawTimezoneOffset = 0; + private int dstSavings; + private boolean dstInEffect; + private boolean touchDevice; + + private VBrowserDetails browserDetails; + private long clientServerTimeDelta; + + /** + * There is no default-theme for this terminal type. + * + * @return Always returns null. + */ + + @Override + public String getDefaultTheme() { + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Terminal#getScreenHeight() + */ + + @Override + public int getScreenHeight() { + return screenHeight; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Terminal#getScreenWidth() + */ + + @Override + public int getScreenWidth() { + return screenWidth; + } + + /** + * Get the browser user-agent string. + * + * @return The raw browser userAgent string + */ + public String getBrowserApplication() { + return browserApplication; + } + + /** + * Gets the IP-address of the web browser. If the application is running + * inside a portlet, this method will return null. + * + * @return IP-address in 1.12.123.123 -format + */ + public String getAddress() { + return address; + } + + /** Get the default locate of the browser. */ + public Locale getLocale() { + return locale; + } + + /** Is the connection made using HTTPS? */ + public boolean isSecureConnection() { + return secureConnection; + } + + /** + * Tests whether the user is using Firefox. + * + * @return true if the user is using Firefox, false if the user is not using + * Firefox or if no information on the browser is present + */ + public boolean isFirefox() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isFirefox(); + } + + /** + * Tests whether the user is using Internet Explorer. + * + * @return true if the user is using Internet Explorer, false if the user is + * not using Internet Explorer or if no information on the browser + * is present + */ + public boolean isIE() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isIE(); + } + + /** + * Tests whether the user is using Safari. + * + * @return true if the user is using Safari, false if the user is not using + * Safari or if no information on the browser is present + */ + public boolean isSafari() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isSafari(); + } + + /** + * Tests whether the user is using Opera. + * + * @return true if the user is using Opera, false if the user is not using + * Opera or if no information on the browser is present + */ + public boolean isOpera() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isOpera(); + } + + /** + * Tests whether the user is using Chrome. + * + * @return true if the user is using Chrome, false if the user is not using + * Chrome or if no information on the browser is present + */ + public boolean isChrome() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isChrome(); + } + + /** + * Tests whether the user is using Chrome Frame. + * + * @return true if the user is using Chrome Frame, false if the user is not + * using Chrome or if no information on the browser is present + */ + public boolean isChromeFrame() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isChromeFrame(); + } + + /** + * Tests whether the user's browser is Chrome Frame capable. + * + * @return true if the user can use Chrome Frame, false if the user can not + * or if no information on the browser is present + */ + public boolean isChromeFrameCapable() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isChromeFrameCapable(); + } + + /** + * Gets the major version of the browser the user is using. + * + * <p> + * Note that Internet Explorer in IE7 compatibility mode might return 8 in + * some cases even though it should return 7. + * </p> + * + * @return The major version of the browser or -1 if not known. + */ + public int getBrowserMajorVersion() { + if (browserDetails == null) { + return -1; + } + + return browserDetails.getBrowserMajorVersion(); + } + + /** + * Gets the minor version of the browser the user is using. + * + * @see #getBrowserMajorVersion() + * + * @return The minor version of the browser or -1 if not known. + */ + public int getBrowserMinorVersion() { + if (browserDetails == null) { + return -1; + } + + return browserDetails.getBrowserMinorVersion(); + } + + /** + * Tests whether the user is using Linux. + * + * @return true if the user is using Linux, false if the user is not using + * Linux or if no information on the browser is present + */ + public boolean isLinux() { + return browserDetails.isLinux(); + } + + /** + * Tests whether the user is using Mac OS X. + * + * @return true if the user is using Mac OS X, false if the user is not + * using Mac OS X or if no information on the browser is present + */ + public boolean isMacOSX() { + return browserDetails.isMacOSX(); + } + + /** + * Tests whether the user is using Windows. + * + * @return true if the user is using Windows, false if the user is not using + * Windows or if no information on the browser is present + */ + public boolean isWindows() { + return browserDetails.isWindows(); + } + + /** + * Returns the browser-reported TimeZone offset in milliseconds from GMT. + * This includes possible daylight saving adjustments, to figure out which + * TimeZone the user actually might be in, see + * {@link #getRawTimezoneOffset()}. + * + * @see WebBrowser#getRawTimezoneOffset() + * @return timezone offset in milliseconds, 0 if not available + */ + public Integer getTimezoneOffset() { + return timezoneOffset; + } + + /** + * Returns the browser-reported TimeZone offset in milliseconds from GMT + * ignoring possible daylight saving adjustments that may be in effect in + * the browser. + * <p> + * You can use this to figure out which TimeZones the user could actually be + * in by calling {@link TimeZone#getAvailableIDs(int)}. + * </p> + * <p> + * If {@link #getRawTimezoneOffset()} and {@link #getTimezoneOffset()} + * returns the same value, the browser is either in a zone that does not + * currently have daylight saving time, or in a zone that never has daylight + * saving time. + * </p> + * + * @return timezone offset in milliseconds excluding DST, 0 if not available + */ + public Integer getRawTimezoneOffset() { + return rawTimezoneOffset; + } + + /** + * Gets the difference in minutes between the browser's GMT TimeZone and + * DST. + * + * @return the amount of minutes that the TimeZone shifts when DST is in + * effect + */ + public int getDSTSavings() { + return dstSavings; + } + + /** + * Determines whether daylight savings time (DST) is currently in effect in + * the region of the browser or not. + * + * @return true if the browser resides at a location that currently is in + * DST + */ + public boolean isDSTInEffect() { + return dstInEffect; + } + + /** + * Returns the current date and time of the browser. This will not be + * entirely accurate due to varying network latencies, but should provide a + * close-enough value for most cases. Also note that the returned Date + * object uses servers default time zone, not the clients. + * + * @return the current date and time of the browser. + * @see #isDSTInEffect() + * @see #getDSTSavings() + * @see #getTimezoneOffset() + */ + public Date getCurrentDate() { + return new Date(new Date().getTime() + clientServerTimeDelta); + } + + /** + * @return true if the browser is detected to support touch events + */ + public boolean isTouchDevice() { + return touchDevice; + } + + /** + * For internal use by AbstractApplicationServlet/AbstractApplicationPortlet + * only. Updates all properties in the class according to the given + * information. + * + * @param sw + * Screen width + * @param sh + * Screen height + * @param tzo + * TimeZone offset in minutes from GMT + * @param rtzo + * raw TimeZone offset in minutes from GMT (w/o DST adjustment) + * @param dstSavings + * the difference between the raw TimeZone and DST in minutes + * @param dstInEffect + * is DST currently active in the region or not? + * @param curDate + * the current date in milliseconds since the epoch + * @param touchDevice + */ + void updateClientSideDetails(String sw, String sh, String tzo, String rtzo, + String dstSavings, String dstInEffect, String curDate, + boolean touchDevice) { + if (sw != null) { + try { + screenHeight = Integer.parseInt(sh); + screenWidth = Integer.parseInt(sw); + } catch (final NumberFormatException e) { + screenHeight = screenWidth = 0; + } + } + if (tzo != null) { + try { + // browser->java conversion: min->ms, reverse sign + timezoneOffset = -Integer.parseInt(tzo) * 60 * 1000; + } catch (final NumberFormatException e) { + timezoneOffset = 0; // default gmt+0 + } + } + if (rtzo != null) { + try { + // browser->java conversion: min->ms, reverse sign + rawTimezoneOffset = -Integer.parseInt(rtzo) * 60 * 1000; + } catch (final NumberFormatException e) { + rawTimezoneOffset = 0; // default gmt+0 + } + } + if (dstSavings != null) { + try { + // browser->java conversion: min->ms + this.dstSavings = Integer.parseInt(dstSavings) * 60 * 1000; + } catch (final NumberFormatException e) { + this.dstSavings = 0; // default no savings + } + } + if (dstInEffect != null) { + this.dstInEffect = Boolean.parseBoolean(dstInEffect); + } + if (curDate != null) { + try { + long curTime = Long.parseLong(curDate); + clientServerTimeDelta = curTime - new Date().getTime(); + } catch (final NumberFormatException e) { + clientServerTimeDelta = 0; + } + } + this.touchDevice = touchDevice; + + } + + /** + * For internal use by AbstractApplicationServlet/AbstractApplicationPortlet + * only. Updates all properties in the class according to the given + * information. + * + * @param request + * the wrapped request to read the information from + */ + void updateRequestDetails(WrappedRequest request) { + locale = request.getLocale(); + address = request.getRemoteAddr(); + secureConnection = request.isSecure(); + String agent = request.getHeader("user-agent"); + + if (agent != null) { + browserApplication = agent; + browserDetails = new VBrowserDetails(agent); + } + + if (request.getParameter("sw") != null) { + updateClientSideDetails(request.getParameter("sw"), + request.getParameter("sh"), request.getParameter("tzo"), + request.getParameter("rtzo"), request.getParameter("dstd"), + request.getParameter("dston"), + request.getParameter("curdate"), + request.getParameter("td") != null); + } + } + + /** + * Checks if the browser is so old that it simply won't work with a Vaadin + * application. Can be used to redirect to an alternative page, show + * alternative content or similar. + * + * When this method returns true chances are very high that the browser + * won't work and it does not make sense to direct the user to the Vaadin + * application. + * + * @return true if the browser won't work, false if not the browser is + * supported or might work + */ + public boolean isTooOldToFunctionProperly() { + if (browserDetails == null) { + // Don't know, so assume it will work + return false; + } + + return browserDetails.isTooOldToFunctionProperly(); + } + +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java new file mode 100644 index 0000000000..cf58f398af --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import com.vaadin.Application; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedRequest; + +/** + * Wrapper for {@link HttpServletRequest}. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedRequest + * @see WrappedHttpServletResponse + */ +public class WrappedHttpServletRequest extends HttpServletRequestWrapper + implements WrappedRequest { + + private final DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a http servlet request and associates with a deployment + * configuration + * + * @param request + * the http servlet request to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedHttpServletRequest(HttpServletRequest request, + DeploymentConfiguration deploymentConfiguration) { + super(request); + this.deploymentConfiguration = deploymentConfiguration; + } + + @Override + public String getRequestPathInfo() { + return getPathInfo(); + } + + @Override + public int getSessionMaxInactiveInterval() { + return getSession().getMaxInactiveInterval(); + } + + @Override + public Object getSessionAttribute(String name) { + return getSession().getAttribute(name); + } + + @Override + public void setSessionAttribute(String name, Object attribute) { + getSession().setAttribute(name, attribute); + } + + /** + * Gets the original, unwrapped HTTP servlet request. + * + * @return the servlet request + */ + public HttpServletRequest getHttpServletRequest() { + return this; + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + @Override + public BrowserDetails getBrowserDetails() { + return new BrowserDetails() { + @Override + public String getUriFragment() { + return null; + } + + @Override + public String getWindowName() { + return null; + } + + @Override + public WebBrowser getWebBrowser() { + WebApplicationContext context = (WebApplicationContext) Application + .getCurrent().getContext(); + return context.getBrowser(); + } + }; + } + + /** + * Helper method to get a <code>WrappedHttpServletRequest</code> from a + * <code>WrappedRequest</code>. Aside from casting, this method also takes + * care of situations where there's another level of wrapping. + * + * @param request + * a wrapped request + * @return a wrapped http servlet request + * @throws ClassCastException + * if the wrapped request doesn't wrap a http servlet request + */ + public static WrappedHttpServletRequest cast(WrappedRequest request) { + if (request instanceof CombinedRequest) { + CombinedRequest combinedRequest = (CombinedRequest) request; + request = combinedRequest.getSecondRequest(); + } + return (WrappedHttpServletRequest) request; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java new file mode 100644 index 0000000000..32b2f352a8 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java @@ -0,0 +1,75 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedResponse; + +/** + * Wrapper for {@link HttpServletResponse}. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedResponse + * @see WrappedHttpServletRequest + */ +public class WrappedHttpServletResponse extends HttpServletResponseWrapper + implements WrappedResponse { + + private DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a http servlet response and an associated deployment configuration + * + * @param response + * the http servlet response to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedHttpServletResponse(HttpServletResponse response, + DeploymentConfiguration deploymentConfiguration) { + super(response); + this.deploymentConfiguration = deploymentConfiguration; + } + + /** + * Gets the original unwrapped <code>HttpServletResponse</code> + * + * @return the unwrapped response + */ + public HttpServletResponse getHttpServletResponse() { + return this; + } + + @Override + public void setCacheTime(long milliseconds) { + doSetCacheTime(this, milliseconds); + } + + // Implementation shared with WrappedPortletResponse + static void doSetCacheTime(WrappedResponse response, long milliseconds) { + if (milliseconds <= 0) { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + } else { + response.setHeader("Cache-Control", "max-age=" + milliseconds + / 1000); + response.setDateHeader("Expires", System.currentTimeMillis() + + milliseconds); + // Required to apply caching in some Tomcats + response.setHeader("Pragma", "cache"); + } + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java new file mode 100644 index 0000000000..a3fa172034 --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java @@ -0,0 +1,217 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.Map; + +import javax.portlet.ClientDataRequest; +import javax.portlet.PortletRequest; +import javax.portlet.ResourceRequest; + +import com.vaadin.Application; +import com.vaadin.terminal.CombinedRequest; +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * Wrapper for {@link PortletRequest} and its subclasses. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedRequest + * @see WrappedPortletResponse + */ +public class WrappedPortletRequest implements WrappedRequest { + + private final PortletRequest request; + private final DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a portlet request and an associated deployment configuration + * + * @param request + * the portlet request to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedPortletRequest(PortletRequest request, + DeploymentConfiguration deploymentConfiguration) { + this.request = request; + this.deploymentConfiguration = deploymentConfiguration; + } + + @Override + public Object getAttribute(String name) { + return request.getAttribute(name); + } + + @Override + public int getContentLength() { + try { + return ((ClientDataRequest) request).getContentLength(); + } catch (ClassCastException e) { + throw new IllegalStateException( + "Content lenght only available for ClientDataRequests"); + } + } + + @Override + public InputStream getInputStream() throws IOException { + try { + return ((ClientDataRequest) request).getPortletInputStream(); + } catch (ClassCastException e) { + throw new IllegalStateException( + "Input data only available for ClientDataRequests"); + } + } + + @Override + public String getParameter(String name) { + return request.getParameter(name); + } + + @Override + public Map<String, String[]> getParameterMap() { + return request.getParameterMap(); + } + + @Override + public void setAttribute(String name, Object o) { + request.setAttribute(name, o); + } + + @Override + public String getRequestPathInfo() { + if (request instanceof ResourceRequest) { + ResourceRequest resourceRequest = (ResourceRequest) request; + String resourceID = resourceRequest.getResourceID(); + if (AbstractApplicationPortlet.RESOURCE_URL_ID.equals(resourceID)) { + String resourcePath = resourceRequest + .getParameter(ApplicationConnection.V_RESOURCE_PATH); + return resourcePath; + } + return resourceID; + } else { + return null; + } + } + + @Override + public int getSessionMaxInactiveInterval() { + return request.getPortletSession().getMaxInactiveInterval(); + } + + @Override + public Object getSessionAttribute(String name) { + return request.getPortletSession().getAttribute(name); + } + + @Override + public void setSessionAttribute(String name, Object attribute) { + request.getPortletSession().setAttribute(name, attribute); + } + + /** + * Gets the original, unwrapped portlet request. + * + * @return the unwrapped portlet request + */ + public PortletRequest getPortletRequest() { + return request; + } + + @Override + public String getContentType() { + try { + return ((ResourceRequest) request).getContentType(); + } catch (ClassCastException e) { + throw new IllegalStateException( + "Content type only available for ResourceRequests"); + } + } + + @Override + public BrowserDetails getBrowserDetails() { + return new BrowserDetails() { + @Override + public String getUriFragment() { + return null; + } + + @Override + public String getWindowName() { + return null; + } + + @Override + public WebBrowser getWebBrowser() { + PortletApplicationContext2 context = (PortletApplicationContext2) Application + .getCurrent().getContext(); + return context.getBrowser(); + } + }; + } + + @Override + public Locale getLocale() { + return request.getLocale(); + } + + @Override + public String getRemoteAddr() { + return null; + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public String getHeader(String string) { + return null; + } + + /** + * Reads a portal property from the portal context of the wrapped request. + * + * @param name + * a string with the name of the portal property to get + * @return a string with the value of the property, or <code>null</code> if + * the property is not defined + */ + public String getPortalProperty(String name) { + return request.getPortalContext().getProperty(name); + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + /** + * Helper method to get a <code>WrappedPortlettRequest</code> from a + * <code>WrappedRequest</code>. Aside from casting, this method also takes + * care of situations where there's another level of wrapping. + * + * @param request + * a wrapped request + * @return a wrapped portlet request + * @throws ClassCastException + * if the wrapped request doesn't wrap a portlet request + */ + public static WrappedPortletRequest cast(WrappedRequest request) { + if (request instanceof CombinedRequest) { + CombinedRequest combinedRequest = (CombinedRequest) request; + request = combinedRequest.getSecondRequest(); + } + return (WrappedPortletRequest) request; + } +} diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java new file mode 100644 index 0000000000..f7ecf26f3c --- /dev/null +++ b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java @@ -0,0 +1,111 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletResponse; +import javax.portlet.ResourceResponse; + +import com.vaadin.terminal.DeploymentConfiguration; +import com.vaadin.terminal.WrappedResponse; + +/** + * Wrapper for {@link PortletResponse} and its subclasses. + * + * @author Vaadin Ltd. + * @since 7.0 + * + * @see WrappedResponse + * @see WrappedPortletRequest + */ +public class WrappedPortletResponse implements WrappedResponse { + private static final DateFormat HTTP_DATE_FORMAT = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); + static { + HTTP_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + private final PortletResponse response; + private DeploymentConfiguration deploymentConfiguration; + + /** + * Wraps a portlet response and an associated deployment configuration + * + * @param response + * the portlet response to wrap + * @param deploymentConfiguration + * the associated deployment configuration + */ + public WrappedPortletResponse(PortletResponse response, + DeploymentConfiguration deploymentConfiguration) { + this.response = response; + this.deploymentConfiguration = deploymentConfiguration; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return ((MimeResponse) response).getPortletOutputStream(); + } + + /** + * Gets the original, unwrapped portlet response. + * + * @return the unwrapped portlet response + */ + public PortletResponse getPortletResponse() { + return response; + } + + @Override + public void setContentType(String type) { + ((MimeResponse) response).setContentType(type); + } + + @Override + public PrintWriter getWriter() throws IOException { + return ((MimeResponse) response).getWriter(); + } + + @Override + public void setStatus(int responseStatus) { + response.setProperty(ResourceResponse.HTTP_STATUS_CODE, + Integer.toString(responseStatus)); + } + + @Override + public void setHeader(String name, String value) { + response.setProperty(name, value); + } + + @Override + public void setDateHeader(String name, long timestamp) { + response.setProperty(name, HTTP_DATE_FORMAT.format(new Date(timestamp))); + } + + @Override + public void setCacheTime(long milliseconds) { + WrappedHttpServletResponse.doSetCacheTime(this, milliseconds); + } + + @Override + public void sendError(int errorCode, String message) throws IOException { + setStatus(errorCode); + getWriter().write(message); + } + + @Override + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/terminal/package.html b/server/src/com/vaadin/terminal/package.html new file mode 100644 index 0000000000..83514a0de5 --- /dev/null +++ b/server/src/com/vaadin/terminal/package.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides classes and interfaces that wrap the terminal-side functionalities +for the server-side application. (FIXME: This could be a little more descriptive and wordy.)</p> + +<h2>Package Specification</h2> + +<!-- Package spec here --> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> |