summaryrefslogtreecommitdiffstats
path: root/server/src/com/vaadin/terminal
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2012-08-13 18:34:33 +0300
committerArtur Signell <artur@vaadin.com>2012-08-13 19:18:33 +0300
commite85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch)
tree9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src/com/vaadin/terminal
parent14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff)
downloadvaadin-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')
-rw-r--r--server/src/com/vaadin/terminal/AbstractClientConnector.java510
-rw-r--r--server/src/com/vaadin/terminal/AbstractErrorMessage.java176
-rw-r--r--server/src/com/vaadin/terminal/AbstractExtension.java76
-rw-r--r--server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java162
-rw-r--r--server/src/com/vaadin/terminal/ApplicationResource.java75
-rw-r--r--server/src/com/vaadin/terminal/ClassResource.java178
-rw-r--r--server/src/com/vaadin/terminal/CombinedRequest.java187
-rw-r--r--server/src/com/vaadin/terminal/CompositeErrorMessage.java112
-rw-r--r--server/src/com/vaadin/terminal/DeploymentConfiguration.java123
-rw-r--r--server/src/com/vaadin/terminal/DownloadStream.java335
-rw-r--r--server/src/com/vaadin/terminal/ErrorMessage.java126
-rw-r--r--server/src/com/vaadin/terminal/Extension.java27
-rw-r--r--server/src/com/vaadin/terminal/ExternalResource.java118
-rw-r--r--server/src/com/vaadin/terminal/FileResource.java174
-rw-r--r--server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java116
-rw-r--r--server/src/com/vaadin/terminal/KeyMapper.java86
-rw-r--r--server/src/com/vaadin/terminal/LegacyPaint.java85
-rw-r--r--server/src/com/vaadin/terminal/Page.java646
-rw-r--r--server/src/com/vaadin/terminal/PaintException.java54
-rw-r--r--server/src/com/vaadin/terminal/PaintTarget.java509
-rw-r--r--server/src/com/vaadin/terminal/RequestHandler.java36
-rw-r--r--server/src/com/vaadin/terminal/Resource.java26
-rw-r--r--server/src/com/vaadin/terminal/Scrollable.java80
-rw-r--r--server/src/com/vaadin/terminal/Sizeable.java242
-rw-r--r--server/src/com/vaadin/terminal/StreamResource.java222
-rw-r--r--server/src/com/vaadin/terminal/StreamVariable.java157
-rw-r--r--server/src/com/vaadin/terminal/SystemError.java82
-rw-r--r--server/src/com/vaadin/terminal/Terminal.java80
-rw-r--r--server/src/com/vaadin/terminal/ThemeResource.java96
-rw-r--r--server/src/com/vaadin/terminal/UserError.java70
-rw-r--r--server/src/com/vaadin/terminal/Vaadin6Component.java44
-rw-r--r--server/src/com/vaadin/terminal/VariableOwner.java85
-rw-r--r--server/src/com/vaadin/terminal/WrappedRequest.java277
-rw-r--r--server/src/com/vaadin/terminal/WrappedResponse.java147
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java1079
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java1623
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java2790
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java143
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java46
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java268
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AddonContext.java80
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java19
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java13
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java38
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java55
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java78
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java11
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java570
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java13
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java39
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java45
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java39
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ClientConnector.java149
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java71
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java122
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java664
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/Constants.java80
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java313
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java417
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java54
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/JsonCodec.java792
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java1022
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java38
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java398
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java170
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java56
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RequestTimer.java43
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ResourceReference.java67
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java172
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RpcManager.java48
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RpcTarget.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java142
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java113
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java120
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java16
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java25
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java17
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java57
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java89
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/UploadException.java15
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java180
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WebBrowser.java462
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java118
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java75
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java217
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java111
-rw-r--r--server/src/com/vaadin/terminal/package.html21
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>
+ * &lt;servlet&gt;
+ * &lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;
+ * &lt;servlet-class&gt;com.vaadin.terminal.gwt.server.GAEApplicationServlet&lt;/servlet-class&gt;
+ * &lt;init-param&gt;
+ * &lt;param-name&gt;application&lt;/param-name&gt;
+ * &lt;param-value&gt;com.vaadin.demo.HelloWorld&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * &lt;/servlet&gt;
+ * </pre>
+ *
+ * Session support must be enabled in appengine-web.xml:
+ *
+ * <pre>
+ * &lt;sessions-enabled&gt;true&lt;/sessions-enabled&gt;
+ * </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>
+ * &lt;cronentries&gt;
+ * &lt;cron&gt;
+ * &lt;url&gt;/HelloWorld/CLEAN&lt;/url&gt;
+ * &lt;description&gt;Clean up sessions&lt;/description&gt;
+ * &lt;schedule&gt;every 2 hours&lt;/schedule&gt;
+ * &lt;/cron&gt;
+ * &lt;/cronentries&gt;
+ * </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>
+ * &lt;static-files&gt;
+ * &lt;include path=&quot;/VAADIN/**&quot; /&gt;
+ * &lt;/static-files&gt;
+ * </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 "&amp;"; // & => &amp;
+ case '>':
+ return "&gt;"; // > => &gt;
+ case '<':
+ return "&lt;"; // < => &lt;
+ case '"':
+ return "&quot;"; // " => &quot;
+ case '\'':
+ return "&apos;"; // ' => &apos;
+ 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>