From 515f6444406cdcfaab6daaef82c72b6e898537f7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 25 Mar 2013 18:47:09 +0200 Subject: [PATCH] Refactor server-side communications (#11192, #7891) * Move UIDL writing from AbstractCommunicationManager to UidlWriter * Move request handling from ACM to RequestHandlers * Move server RPC message handling to ServerRpcHandler * Move portlet event notifications to PortletListenerNotifier (a RequestHandler) * Communication handlers reside in c.v.server.communication Change-Id: I087e923dbdf88c6b3fcaafbdb7f685d9d3dad0c1 --- .../server/AbstractCommunicationManager.java | 2480 +---------------- .../vaadin/server/CommunicationManager.java | 2 +- .../vaadin/server/ComponentSizeValidator.java | 4 +- .../com/vaadin/server/DragAndDropService.java | 6 +- .../com/vaadin/server/JsonPaintTarget.java | 8 +- .../server/PortletCommunicationManager.java | 2 +- .../com/vaadin/server/ServerRpcManager.java | 2 +- .../vaadin/server/ServletPortletHelper.java | 21 +- .../src/com/vaadin/server/VaadinPortlet.java | 78 +- .../src/com/vaadin/server/VaadinServlet.java | 60 +- server/src/com/vaadin/server/WebBrowser.java | 2 +- .../AbstractStreamingEvent.java | 3 +- .../server/communication/ClientRpcWriter.java | 141 + .../ConnectorHierarchyWriter.java | 81 + .../communication/ConnectorTypeWriter.java | 73 + .../communication/FileUploadHandler.java | 586 ++++ .../communication/HeartbeatHandler.java | 73 + .../communication/LegacyUidlWriter.java | 118 + .../server/communication/LocaleWriter.java | 204 ++ .../server/communication/MetadataWriter.java | 137 + .../PortletListenerNotifier.java | 89 + .../communication/PublishedFileHandler.java | 145 + .../server/communication/ResourceWriter.java | 111 + .../communication/ServerRpcHandler.java | 494 ++++ .../communication/SharedStateWriter.java | 75 + .../StreamingEndEventImpl.java | 3 +- .../StreamingErrorEventImpl.java | 3 +- .../StreamingProgressEventImpl.java | 3 +- .../StreamingStartEventImpl.java | 3 +- .../server/communication/UIInitHandler.java | 249 ++ .../communication/UidlRequestHandler.java | 274 ++ .../server/communication/UidlWriter.java | 315 +++ .../src/com/vaadin/ui/ConnectorTracker.java | 17 + .../TestSimpleMultiPartInputStream.java | 2 +- .../server/TestStreamVariableMapping.java | 2 +- 35 files changed, 3370 insertions(+), 2496 deletions(-) rename server/src/com/vaadin/server/{ => communication}/AbstractStreamingEvent.java (95%) create mode 100644 server/src/com/vaadin/server/communication/ClientRpcWriter.java create mode 100644 server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java create mode 100644 server/src/com/vaadin/server/communication/ConnectorTypeWriter.java create mode 100644 server/src/com/vaadin/server/communication/FileUploadHandler.java create mode 100644 server/src/com/vaadin/server/communication/HeartbeatHandler.java create mode 100644 server/src/com/vaadin/server/communication/LegacyUidlWriter.java create mode 100644 server/src/com/vaadin/server/communication/LocaleWriter.java create mode 100644 server/src/com/vaadin/server/communication/MetadataWriter.java create mode 100644 server/src/com/vaadin/server/communication/PortletListenerNotifier.java create mode 100644 server/src/com/vaadin/server/communication/PublishedFileHandler.java create mode 100644 server/src/com/vaadin/server/communication/ResourceWriter.java create mode 100644 server/src/com/vaadin/server/communication/ServerRpcHandler.java create mode 100644 server/src/com/vaadin/server/communication/SharedStateWriter.java rename server/src/com/vaadin/server/{ => communication}/StreamingEndEventImpl.java (91%) rename server/src/com/vaadin/server/{ => communication}/StreamingErrorEventImpl.java (93%) rename server/src/com/vaadin/server/{ => communication}/StreamingProgressEventImpl.java (92%) rename server/src/com/vaadin/server/{ => communication}/StreamingStartEventImpl.java (93%) create mode 100644 server/src/com/vaadin/server/communication/UIInitHandler.java create mode 100644 server/src/com/vaadin/server/communication/UidlRequestHandler.java create mode 100644 server/src/com/vaadin/server/communication/UidlWriter.java diff --git a/server/src/com/vaadin/server/AbstractCommunicationManager.java b/server/src/com/vaadin/server/AbstractCommunicationManager.java index 17bbbda737..29dc5b2f81 100644 --- a/server/src/com/vaadin/server/AbstractCommunicationManager.java +++ b/server/src/com/vaadin/server/AbstractCommunicationManager.java @@ -16,36 +16,17 @@ package com.vaadin.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.Type; +import java.io.Writer; 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; @@ -54,37 +35,19 @@ import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import javax.servlet.http.HttpServletResponse; - -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import com.vaadin.annotations.JavaScript; -import com.vaadin.annotations.PreserveOnRefresh; -import com.vaadin.annotations.StyleSheet; import com.vaadin.server.ClientConnector.ConnectorErrorEvent; -import com.vaadin.server.ComponentSizeValidator.InvalidLayout; -import com.vaadin.server.ServerRpcManager.RpcInvocationException; -import com.vaadin.server.StreamVariable.StreamingEndEvent; -import com.vaadin.server.StreamVariable.StreamingErrorEvent; +import com.vaadin.server.communication.LocaleWriter; import com.vaadin.shared.ApplicationConstants; -import com.vaadin.shared.Connector; import com.vaadin.shared.JavaScriptConnectorState; -import com.vaadin.shared.Version; -import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; -import com.vaadin.shared.communication.MethodInvocation; -import com.vaadin.shared.communication.ServerRpc; import com.vaadin.shared.communication.SharedState; -import com.vaadin.shared.communication.UidlValue; -import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.ui.Component; import com.vaadin.ui.ConnectorTracker; import com.vaadin.ui.HasComponents; -import com.vaadin.ui.LegacyComponent; import com.vaadin.ui.SelectiveRenderer; import com.vaadin.ui.UI; -import com.vaadin.ui.Window; /** * This is a common base class for the server-side implementations of the @@ -100,7 +63,8 @@ import com.vaadin.ui.Window; @SuppressWarnings("serial") public abstract class AbstractCommunicationManager implements Serializable { - private static final String DASHDASH = "--"; + // TODO PUSH move + public static final String WRITE_SECURITY_TOKEN_FLAG = "writeSecurityToken"; private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler(); @@ -122,47 +86,24 @@ public abstract class AbstractCommunicationManager implements Serializable { String details, String outOfSyncURL) throws IOException; } - static class UploadInterruptedException extends Exception { - public UploadInterruptedException() { - super("Upload interrupted by other thread"); - } - } - - // 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'; - + // TODO Refactor (#11410) private final HashMap uiToClientCache = new HashMap(); - 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; - /** * The session this communication manager is used for */ private final VaadinSession session; + // TODO Refactor to UI shared state (#11378) private List locales; - private int pendingLocalesIndex; - - private int timeoutInterval = -1; - + // TODO Move to VaadinSession (#11409) private DragAndDropService dragAndDropService; + // TODO Refactor (#11412) private String requestThemeName; - private int maxInactiveInterval; - - private ClientConnector highlightedConnector; - + // TODO Refactor (#11413) private Map> publishedFileContexts = new HashMap>(); /** @@ -172,6 +113,7 @@ public abstract class AbstractCommunicationManager implements Serializable { */ public AbstractCommunicationManager(VaadinSession session) { this.session = session; + // TODO Common to all sessions - handle at VaadinService level session.addRequestHandler(getBootstrapHandler()); session.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER); session.addRequestHandler(CONNECTOR_RESOURCE_HANDLER); @@ -182,557 +124,6 @@ public abstract class AbstractCommunicationManager implements Serializable { return session; } - private static final int LF = "\n".getBytes()[0]; - - private static final String CRLF = "\r\n"; - - private static final String UTF8 = "UTF-8"; - - 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(VaadinRequest request, - VaadinResponse 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.getBytes(UTF8).length + CRLF.length()); - if (readLine.startsWith("Content-Disposition:") - && readLine.indexOf("filename=") > 0) { - rawfilename = readLine.replaceAll(".*filename=", ""); - char quote = rawfilename.charAt(0); - rawfilename = rawfilename.substring(1); - rawfilename = rawfilename.substring(0, - rawfilename.indexOf(quote)); - 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() + CRLF.length()); - - /* - * 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) { - session.lock(); - try { - handleConnectorRelatedException(owner, e); - } finally { - session.unlock(); - } - } - 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(VaadinRequest request, - VaadinResponse 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) { - session.lock(); - try { - handleConnectorRelatedException(owner, e); - } finally { - session.unlock(); - } - } - 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 VaadinSession session = getSession(); - - OutputStream out = null; - int totalBytes = 0; - StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( - filename, type, contentLength); - try { - boolean listenProgress; - session.lock(); - try { - streamVariable.streamingStarted(startedEvent); - out = streamVariable.getOutputStream(); - listenProgress = streamVariable.listenProgress(); - } finally { - session.unlock(); - } - - // 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 - session.lock(); - try { - StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( - filename, type, contentLength, totalBytes); - streamVariable.onProgress(progressEvent); - } finally { - session.unlock(); - } - } - if (streamVariable.isInterrupted()) { - throw new UploadInterruptedException(); - } - } - - // upload successful - out.close(); - StreamingEndEvent event = new StreamingEndEventImpl(filename, type, - totalBytes); - session.lock(); - try { - streamVariable.streamingFinished(event); - } finally { - session.unlock(); - } - - } catch (UploadInterruptedException e) { - // Download interrupted by application code - tryToCloseStream(out); - StreamingErrorEvent event = new StreamingErrorEventImpl(filename, - type, contentLength, totalBytes, e); - session.lock(); - try { - streamVariable.streamingFailed(event); - } finally { - session.unlock(); - } - // 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); - session.lock(); - try { - StreamingErrorEvent event = new StreamingErrorEventImpl( - filename, type, contentLength, totalBytes, e); - streamVariable.streamingFailed(event); - // throw exception for terminal to be handled (to be passed to - // terminalErrorHandler) - throw new UploadException(e); - } finally { - session.unlock(); - } - } - 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(VaadinRequest request, - VaadinResponse 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("download handled"); - outWriter.flush(); - out.close(); - } - - /** - * Internally process a UIDL request from the client. - * - * This method calls - * {@link #handleVariables(VaadinRequest, VaadinResponse, Callback, VaadinSession, UI)} - * 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 session that has - * already been closed. - * - * The method handleUidlRequest(...) in subclasses should call this method. - * - * TODO better documentation - * - * @param request - * @param response - * @param callback - * @param uI - * target window for the UIDL request, can be null if target not - * found - * @throws IOException - * @throws InvalidUIDLSecurityKeyException - * @throws JSONException - */ - public void handleUidlRequest(VaadinRequest request, - VaadinResponse response, Callback callback, UI uI) - throws IOException, InvalidUIDLSecurityKeyException, JSONException { - - checkWidgetsetVersion(request); - requestThemeName = request.getParameter("theme"); - maxInactiveInterval = request.getWrappedSession() - .getMaxInactiveInterval(); - // repaint requested or session has timed out and new one is created - boolean repaintAll; - final OutputStream out; - - repaintAll = (request - .getParameter(ApplicationConstants.URL_PARAMETER_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(ApplicationConstants.PARAM_ANALYZE_LAYOUTS) != null); - - String pid = request - .getParameter(ApplicationConstants.PARAM_HIGHLIGHT_CONNECTOR); - if (pid != null) { - highlightedConnector = uI.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 session - // in order to guarantee that no parallel variable handling is - // made - session.lock(); - try { - - // Verify that there's an UI - if (uI == null) { - // This should not happen, no windows exists but - // session is still open. - getLogger().warning("Could not get UI for session"); - return; - } - - session.setLastRequestTimestamp(System.currentTimeMillis()); - - // Change all variables based on request parameters - if (!handleVariables(request, response, callback, session, uI)) { - - // var inconsistency; the client is probably out-of-sync - SystemMessages ci = response.getService().getSystemMessages( - uI.getLocale(), request); - 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, uI, analyzeLayouts); - postPaint(uI); - } finally { - session.unlock(); - } - - outWriter.close(); - requestThemeName = null; - } - - /** - * Checks that the version reported by the client (widgetset) matches that - * of the server. - * - * @param request - */ - private void checkWidgetsetVersion(VaadinRequest request) { - String widgetsetVersion = request.getParameter("v-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 - * session - * - * @param uI - * - */ - protected void postPaint(UI uI) { - // Remove connectors that have been detached from the session during - // handling of the request - uI.getConnectorTracker().cleanConnectorMap(); - } - - protected void highlightConnector(ClientConnector highlightedConnector) { - StringBuilder sb = new StringBuilder(); - sb.append("*** Debug details of a connector: *** \n"); - sb.append("Type: "); - sb.append(highlightedConnector.getClass().getName()); - sb.append("\nId:"); - sb.append(highlightedConnector.getConnectorId()); - if (highlightedConnector instanceof Component) { - Component component = (Component) highlightedConnector; - if (component.getCaption() != null) { - sb.append("\nCaption:"); - sb.append(component.getCaption()); - } - } - printHighlightedConnectorHierarchy(sb, highlightedConnector); - getLogger().info(sb.toString()); - } - - protected void printHighlightedConnectorHierarchy(StringBuilder sb, - ClientConnector connector) { - LinkedList h = new LinkedList(); - h.add(connector); - ClientConnector parent = connector.getParent(); - while (parent != null) { - h.addFirst(parent); - parent = parent.getParent(); - } - - sb.append("\nConnector hierarchy:\n"); - VaadinSession session2 = connector.getUI().getSession(); - sb.append(session2.getClass().getName()); - sb.append("("); - sb.append(session2.getClass().getSimpleName()); - sb.append(".java"); - sb.append(":1)"); - int l = 1; - for (ClientConnector connector2 : h) { - sb.append("\n"); - for (int i = 0; i < l; i++) { - sb.append(" "); - } - l++; - Class connectorClass = connector2 - .getClass(); - Class topClass = connectorClass; - while (topClass.getEnclosingClass() != null) { - topClass = topClass.getEnclosingClass(); - } - sb.append(connectorClass.getName()); - 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(VaadinRequest request, - VaadinResponse response, Callback callback, boolean repaintAll, - final PrintWriter outWriter, UI uI, boolean analyzeLayouts) - throws PaintException, IOException, JSONException { - openJsonMessage(outWriter, response); - - // security key - Object writeSecurityTokenFlag = request - .getAttribute(WRITE_SECURITY_TOKEN_FLAG); - - if (writeSecurityTokenFlag != null) { - outWriter.print(getSecurityKeyUIDL(request)); - } - - writeUidlResponse(request, repaintAll, outWriter, uI, analyzeLayouts); - - closeJsonMessage(outWriter); - - outWriter.close(); - - } - /** * Gets the security key (and generates one if needed) as UIDL. * @@ -769,443 +160,10 @@ public abstract class AbstractCommunicationManager implements Serializable { return seckey; } - @SuppressWarnings("unchecked") - public void writeUidlResponse(VaadinRequest request, boolean repaintAll, - final PrintWriter outWriter, UI ui, boolean analyzeLayouts) - throws PaintException, JSONException { - ArrayList dirtyVisibleConnectors = new ArrayList(); - VaadinSession session = ui.getSession(); - // Paints components - ConnectorTracker uiConnectorTracker = ui.getConnectorTracker(); - getLogger().log(Level.FINE, "* Creating response to client"); - if (repaintAll) { - getClientCache(ui).clear(); - uiConnectorTracker.markAllConnectorsDirty(); - uiConnectorTracker.markAllClientSidesUninitialized(); - - // Reset sent locales - locales = null; - requireLocale(session.getLocale().toString()); - } - - dirtyVisibleConnectors - .addAll(getDirtyVisibleConnectors(uiConnectorTracker)); - - getLogger().log(Level.FINE, "Found {0} dirty connectors to paint", - dirtyVisibleConnectors.size()); - for (ClientConnector connector : dirtyVisibleConnectors) { - boolean initialized = uiConnectorTracker - .isClientSideInitialized(connector); - connector.beforeClientResponse(!initialized); - } - - uiConnectorTracker.setWritingResponse(true); - try { - outWriter.print("\"changes\":["); - - List invalidComponentRelativeSizes = null; - - JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, - !repaintAll); - legacyPaint(paintTarget, dirtyVisibleConnectors); - - if (analyzeLayouts) { - invalidComponentRelativeSizes = ComponentSizeValidator - .validateComponentRelativeSizes(ui.getContent(), null, - null); - - // Also check any existing subwindows - if (ui.getWindows() != null) { - for (Window subWindow : ui.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) { - // encode and send shared state - try { - JSONObject stateJson = connector.encodeState(); - - if (stateJson != null && stateJson.length() != 0) { - 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 (isConnectorVisibleToClient(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 - - uiConnectorTracker.markAllConnectorsClean(); - - // send server to client RPC calls for components in the UI, in call - // order - - // collect RPC calls from components in the UI in the order in - // which they were performed, remove the calls from components - - LinkedList rpcPendingQueue = new LinkedList( - dirtyVisibleConnectors); - List 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()); - // } - // } - EncodeResult encodeResult = JsonCodec.encode( - invocation.getParameters()[i], - referenceParameter, parameterType, - ui.getConnectorTracker()); - paramJson.put(encodeResult.getEncodedValue()); - } - 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 = request.getService().getSystemMessages( - ui.getLocale(), request); - - // 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 i = paintTarget.getUsedResources() - .iterator(); i.hasNext();) { - final String resource = (String) i.next(); - InputStream is = null; - try { - is = getThemeResourceAsStream(ui, getTheme(ui), 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> usedClientConnectors = paintTarget - .getUsedClientConnectors(); - boolean typeMappingsOpen = false; - ClientCache clientCache = getClientCache(ui); - - List> newConnectorTypes = new ArrayList>(); - - for (Class 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 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) 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>() { - @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 scriptDependencies = new ArrayList(); - List styleDependencies = new ArrayList(); - - for (Class class1 : newConnectorTypes) { - JavaScript jsAnnotation = class1 - .getAnnotation(JavaScript.class); - if (jsAnnotation != null) { - for (String uri : jsAnnotation.value()) { - scriptDependencies.add(registerDependency(uri, class1)); - } - } - - StyleSheet styleAnnotation = class1 - .getAnnotation(StyleSheet.class); - if (styleAnnotation != null) { - for (String uri : styleAnnotation.value()) { - styleDependencies.add(registerDependency(uri, 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); - } - - for (ClientConnector connector : dirtyVisibleConnectors) { - uiConnectorTracker.markClientSideInitialized(connector); - } - - assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended."; - - writePerformanceData(outWriter); - } finally { - uiConnectorTracker.setWritingResponse(false); - } - } - + /** + * @deprecated As of 7.1. See #11411. + */ + @Deprecated public static JSONObject encodeState(ClientConnector connector, SharedState state) throws JSONException { UI uI = connector.getUI(); @@ -1243,8 +201,11 @@ public abstract class AbstractCommunicationManager implements Serializable { * Resolves a dependency URI, registering the URI with this * {@code AbstractCommunicationManager} if needed and returns a fully * qualified URI. + * + * @deprecated As of 7.1. See #11413. */ - private String registerDependency(String resourceUri, Class context) { + @Deprecated + public String registerDependency(String resourceUri, Class context) { try { URI uri = new URI(resourceUri); String protocol = uri.getScheme(); @@ -1268,6 +229,14 @@ public abstract class AbstractCommunicationManager implements Serializable { } } + /** + * @deprecated As of 7.1. See #11413. + */ + @Deprecated + public Map> getDependencies() { + return publishedFileContexts; + } + private String registerPublishedFile(String name, Class context) { synchronized (publishedFileContexts) { // Add to map of names accepted by servePublishedFile @@ -1288,75 +257,10 @@ public abstract class AbstractCommunicationManager implements Serializable { } /** - * Adds the performance timing data (used by TestBench 3) to the UIDL - * response. + * @deprecated As of 7.1. See #11410. */ - private void writePerformanceData(final PrintWriter outWriter) { - outWriter.write(String.format(", \"timings\":[%d, %d]", - session.getCumulativeRequestDuration(), - session.getLastRequestDuration())); - } - - private void legacyPaint(PaintTarget paintTarget, - ArrayList dirtyVisibleConnectors) - throws PaintException { - List legacyComponents = new ArrayList(); - for (Connector connector : dirtyVisibleConnectors) { - // All Components that want to use paintContent must implement - // LegacyComponent - if (connector instanceof LegacyComponent) { - legacyComponents.add((LegacyComponent) connector); - } - } - sortByHierarchy((List) legacyComponents); - for (LegacyComponent c : legacyComponents) { - if (getLogger().isLoggable(Level.FINE)) { - getLogger().log( - Level.FINE, - "Painting LegacyComponent {0}@{1}", - new Object[] { 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 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() { - - @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(UI uI) { + @Deprecated + public ClientCache getClientCache(UI uI) { Integer uiId = Integer.valueOf(uI.getUIId()); ClientCache cache = uiToClientCache.get(uiId); if (cache == null) { @@ -1372,11 +276,14 @@ public abstract class AbstractCommunicationManager implements Serializable { * of connectors, the contextual visibility of its first Component ancestor * is used. If no Component ancestor is found, the connector is not visible. * + * @deprecated As of 7.1. See #11411. + * * @param connector * The connector to check * @return true if the connector is visible to the client, * false otherwise */ + @Deprecated public static boolean isConnectorVisibleToClient(ClientConnector connector) { if (connector instanceof Component) { return isComponentVisibleToClient((Component) connector); @@ -1394,10 +301,13 @@ public abstract class AbstractCommunicationManager implements Serializable { * Checks if the component should be visible to the client. Returns false if * the child should not be sent to the client, true otherwise. * + * @deprecated As of 7.1. See #11411. + * * @param child * The child to check * @return true if the child is visible to the client, false otherwise */ + @Deprecated public static boolean isComponentVisibleToClient(Component child) { if (!child.isVisible()) { return false; @@ -1423,71 +333,18 @@ public abstract class AbstractCommunicationManager implements Serializable { } } - private static class NullIterator implements Iterator { - - @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 + * @deprecated As of 7.1. Likely to be removed or replaced in the future. */ - private List collectPendingRpcCalls( - List rpcPendingQueue) { - List pendingInvocations = new ArrayList(); - for (ClientConnector connector : rpcPendingQueue) { - List paintablePendingRpc = connector - .retrievePendingRpcCalls(); - if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { - List oldPendingRpc = pendingInvocations; - int totalCalls = pendingInvocations.size() - + paintablePendingRpc.size(); - pendingInvocations = new ArrayList( - 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) 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(UI uI, + @Deprecated + public abstract InputStream getThemeResourceAsStream(UI uI, String themeName, String resource); - private int getTimeoutInterval() { - return maxInactiveInterval; - } - - private String getTheme(UI uI) { + /** + * @deprecated As of 7.1. See #11412. + */ + @Deprecated + public String getTheme(UI uI) { String themeName = uI.getTheme(); String requestThemeName = getRequestTheme(); @@ -1495,401 +352,20 @@ public abstract class AbstractCommunicationManager implements Serializable { themeName = requestThemeName; } if (themeName == null) { - themeName = VaadinServlet.getDefaultTheme(); - } - return themeName; - } - - private String getRequestTheme() { - return requestThemeName; - } - - /** - * Returns false if the cross site request forgery protection is turned off. - * - * @param session - * @return false if the XSRF is turned off, true otherwise - */ - public boolean isXSRFEnabled(VaadinSession session) { - return session.getConfiguration().isXsrfProtectionEnabled(); - } - - /** - * 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(VaadinRequest request, - VaadinResponse response, Callback callback, VaadinSession session, - UI uI) 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(session)) { - 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 - .getWrappedSession() - .getAttribute( - ApplicationConstants.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, uI, 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, uI, 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 uI - * the UI 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(VaadinRequest source, UI uI, final String burst) { - boolean success = true; - try { - Set enabledConnectors = new HashSet(); - - List invocations = parseInvocations( - uI.getConnectorTracker(), burst); - for (MethodInvocation invocation : invocations) { - final ClientConnector connector = getConnector(uI, - 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(uI, - invocation.getConnectorId()); - if (connector == null) { - getLogger() - .log(Level.WARNING, - "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})", - new Object[] { invocation.getConnectorId(), - invocation.getInterfaceName(), - invocation.getMethodName() }); - 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 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) { - handleConnectorRelatedException(connector, e); - } - } else { - - // All code below is for legacy variable changes - LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; - Map 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) { - handleConnectorRelatedException(connector, e); - } - } - } - } catch (JSONException e) { - getLogger().log(Level.WARNING, - "Unable to parse RPC call from the client: {0}", - e.getMessage()); - // TODO or return success = false? - throw new RuntimeException(e); - } - - return success; - } - - /** - * Handles an exception that occurred when processing Rpc calls or a file - * upload. - * - * @param ui - * The UI where the exception occured - * @param throwable - * The exception - * @param connector - * The Rpc target - */ - private void handleConnectorRelatedException(ClientConnector connector, - Throwable throwable) { - ErrorEvent errorEvent = new ConnectorErrorEvent(connector, throwable); - ErrorHandler handler = ErrorEvent.findErrorHandler(connector); - handler.error(errorEvent); - } - - /** - * 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 parseInvocations( - ConnectorTracker connectorTracker, final String burst) - throws JSONException { - JSONArray invocationsJson = new JSONArray(burst); - - ArrayList invocations = new ArrayList(); - - 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 if the invocation was a legacy invocation and it - // was merged with the previous one or if the invocation was - // rejected because of an error. - 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); - - if (connectorTracker.getConnector(connectorId) == null - && !connectorId - .equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) { - getLogger() - .log(Level.WARNING, - "RPC call to " - + interfaceName - + "." - + methodName - + " received for connector " - + connectorId - + " but no such connector could be found. Resynchronizing client."); - // This is likely an out of sync issue (client tries to update a - // connector which is not present). Force resync. - connectorTracker.markAllConnectorsDirty(); - return null; - } - - 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 { - ClientConnector connector = connectorTracker.getConnector(connectorId); - - ServerRpcManager rpcManager = connector.getRpcManager(interfaceName); - if (rpcManager == null) { - /* - * Security: Don't even decode the json parameters if no RpcManager - * corresponding to the received method invocation has been - * registered. - */ - getLogger() - .log(Level.WARNING, - "Ignoring RPC call to {0}.{1} in connector {2} ({3}) as no RPC implementation is regsitered", - new Object[] { interfaceName, methodName, - connector.getClass().getName(), connectorId }); - return null; - } - - // Use interface from RpcManager instead of loading the class based on - // the string name to avoid problems with OSGi - Class rpcInterface = rpcManager.getRpcInterface(); - - ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( - connectorId, rpcInterface, 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); + themeName = VaadinServlet.getDefaultTheme(); } - invocation.setParameters(parameters); - return invocation; + return themeName; } - protected void changeVariables(Object source, final VariableOwner owner, - Map m) { - owner.changeVariables(source, m); + private String getRequestTheme() { + return requestThemeName; } - protected ClientConnector getConnector(UI uI, String connectorId) { + /** + * @deprecated As of 7.1. See #11411. + */ + @Deprecated + public ClientConnector getConnector(UI uI, String connectorId) { ClientConnector c = uI.getConnectorTracker().getConnector(connectorId); if (c == null && connectorId.equals(getDragAndDropService().getConnectorId())) { @@ -1899,263 +375,29 @@ public abstract class AbstractCommunicationManager implements Serializable { return c; } - private DragAndDropService getDragAndDropService() { + /** + * @deprecated As of 7.1. See #11409. + */ + @Deprecated + public 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(VaadinRequest 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; - } - - /** - * 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().log(Level.WARNING, - "Unable to get default date pattern for locale {0}", l); - 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 - } - - protected void closeJsonMessage(PrintWriter outWriter) { - outWriter.print("}]"); - } - - /** - * Writes the opening of JSON message to be sent to client. + * @deprecated As of 7.1. See #11378. * * @param outWriter - * @param response - */ - protected void openJsonMessage(PrintWriter outWriter, - VaadinResponse 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 - * UI window for which dirty components is to be fetched - * @return */ - private ArrayList getDirtyVisibleConnectors( - ConnectorTracker connectorTracker) { - ArrayList dirtyConnectors = new ArrayList(); - for (ClientConnector c : connectorTracker.getDirtyConnectors()) { - if (isConnectorVisibleToClient(c)) { - dirtyConnectors.add(c); - } - } - - return dirtyConnectors; + @Deprecated + public void printLocaleDeclarations(Writer writer) throws IOException { + new LocaleWriter().write(locales, writer); } /** @@ -2165,15 +407,17 @@ public abstract class AbstractCommunicationManager implements Serializable { * the need to use the {@link Locale} class and all the framework behind it * on the client. * + * @deprecated As of 7.1. See #11378. + * * @see Locale#toString() * * @param value */ + @Deprecated public void requireLocale(String value) { if (locales == null) { locales = new ArrayList(); locales.add(session.getLocale().toString()); - pendingLocalesIndex = 0; } if (!locales.contains(value)) { locales.add(value); @@ -2181,32 +425,23 @@ public abstract class AbstractCommunicationManager implements Serializable { } /** - * Constructs a {@link Locale} instance to be sent to the client based on a - * short locale description string. - * - * @see #requireLocale(String) - * - * @param value - * @return + * @deprecated As of 7.1. See #11378. */ - 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]); - } + @Deprecated + public void resetLocales() { + locales = null; } - protected class InvalidUIDLSecurityKeyException extends + /** + * @deprecated As of 7.1. Will be removed in the future. + */ + @Deprecated + public static class InvalidUIDLSecurityKeyException extends GeneralSecurityException { - InvalidUIDLSecurityKeyException(String message) { + public InvalidUIDLSecurityKeyException(String message) { super(message); } - } private final HashMap, Integer> typeToKey = new HashMap, Integer>(); @@ -2214,7 +449,11 @@ public abstract class AbstractCommunicationManager implements Serializable { private BootstrapHandler bootstrapHandler; - String getTagForType(Class class1) { + /** + * @deprecated As of 7.1. Will be removed in the future. + */ + @Deprecated + public String getTagForType(Class class1) { Integer id = typeToKey.get(class1); if (id == null) { id = nextTypeKey++; @@ -2232,8 +471,11 @@ public abstract class AbstractCommunicationManager implements Serializable { * to know. * * TODO make customlayout templates (from theme) to be cached here. + * + * @deprecated As of 7.1. See #11410. */ - class ClientCache implements Serializable { + @Deprecated + public class ClientCache implements Serializable { private final Set res = new HashSet(); @@ -2242,7 +484,7 @@ public abstract class AbstractCommunicationManager implements Serializable { * @param paintable * @return true if the given class was added to cache */ - boolean cache(Object object) { + public boolean cache(Object object) { return res.add(object); } @@ -2252,6 +494,10 @@ public abstract class AbstractCommunicationManager implements Serializable { } + /** + * @deprecated As of 7.1. See #11411. + */ + @Deprecated public String getStreamVariableTargetUrl(ClientConnector owner, String name, StreamVariable value) { /* @@ -2280,11 +526,6 @@ public abstract class AbstractCommunicationManager implements Serializable { } - public void cleanStreamVariable(ClientConnector owner, String name) { - owner.getUI().getConnectorTracker() - .cleanStreamVariable(owner.getConnectorId(), name); - } - /** * Gets the bootstrap handler that should be used for generating the pages * bootstrapping applications for this communication manager. @@ -2335,7 +576,10 @@ public abstract class AbstractCommunicationManager implements Serializable { * @see RequestHandler * * @since 7.0 + * + * @deprecated As of 7.1. Should be moved to VaadinService. */ + @Deprecated protected boolean handleOtherRequest(VaadinRequest request, VaadinResponse response) throws IOException { // Use a copy to avoid ConcurrentModificationException @@ -2349,530 +593,44 @@ public abstract class AbstractCommunicationManager implements Serializable { return false; } - public void handleBrowserDetailsRequest(VaadinRequest request, - VaadinResponse response, VaadinSession session) throws IOException { - - session.lock(); - - try { - assert UI.getCurrent() == null; - - response.setContentType("application/json; charset=UTF-8"); - - UI uI = getBrowserDetailsUI(request, session); - - JSONObject params = new JSONObject(); - params.put(UIConstants.UI_ID_PARAMETER, uI.getUIId()); - String initialUIDL = getInitialUIDL(request, uI); - 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 (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - session.unlock(); - } - } - - private UI getBrowserDetailsUI(VaadinRequest request, VaadinSession session) { - VaadinService vaadinService = request.getService(); - - List uiProviders = session.getUIProviders(); - - UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent( - request); - - UIProvider provider = null; - Class uiClass = null; - for (UIProvider p : uiProviders) { - // Check for existing LegacyWindow - if (p instanceof LegacyApplicationUIProvider) { - LegacyApplicationUIProvider legacyProvider = (LegacyApplicationUIProvider) p; - - UI existingUi = legacyProvider - .getExistingUI(classSelectionEvent); - if (existingUi != null) { - reinitUI(existingUi, request); - return existingUi; - } - } - - uiClass = p.getUIClass(classSelectionEvent); - if (uiClass != null) { - provider = p; - break; - } - } - - if (provider == null || uiClass == null) { - return null; - } - - // Check for an existing UI based on window.name - - // Special parameter sent by vaadinBootstrap.js - String windowName = request.getParameter("v-wn"); - - Map retainOnRefreshUIs = session - .getPreserveOnRefreshUIs(); - if (windowName != null && !retainOnRefreshUIs.isEmpty()) { - // Check for a known UI - - Integer retainedUIId = retainOnRefreshUIs.get(windowName); - - if (retainedUIId != null) { - UI retainedUI = session.getUIById(retainedUIId.intValue()); - if (uiClass.isInstance(retainedUI)) { - reinitUI(retainedUI, request); - return retainedUI; - } else { - getLogger().log( - Level.INFO, - "Not using retained UI in {0} because retained UI was of type {1}" - + " but {2} is expected for the request.", - new Object[] { windowName, retainedUI.getClass(), - uiClass }); - } - } - } - - // No existing UI found - go on by creating and initializing one - - Integer uiId = Integer.valueOf(session.getNextUIid()); - - // Explicit Class.cast to detect if the UIProvider does something - // unexpected - UICreateEvent event = new UICreateEvent(request, uiClass, uiId); - UI ui = uiClass.cast(provider.createInstance(event)); - - // Initialize some fields for a newly created UI - if (ui.getSession() != session) { - // Session already set for LegacyWindow - ui.setSession(session); - } - - // Set thread local here so it is available in init - UI.setCurrent(ui); - - ui.doInit(request, uiId.intValue()); - - session.addUI(ui); - - // Remember if it should be remembered - if (vaadinService.preserveUIOnRefresh(provider, event)) { - // Remember this UI - if (windowName == null) { - getLogger() - .log(Level.WARNING, - "There is no window.name available for UI {0} that should be preserved.", - uiClass); - } else { - session.getPreserveOnRefreshUIs().put(windowName, uiId); - } - } - - return ui; - } - - /** - * Updates a UI that has already been initialized but is now loaded again, - * e.g. because of {@link PreserveOnRefresh}. - * - * @param ui - * @param request - */ - private void reinitUI(UI ui, VaadinRequest request) { - UI.setCurrent(ui); - - // Fire fragment change if the fragment has changed - String location = request.getParameter("v-loc"); - if (location != null) { - ui.getPage().updateLocation(location); - } - } - - /** - * 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 uI - * the UI 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(VaadinRequest request, UI uI) - throws PaintException, JSONException { - // TODO maybe unify writeUidlResponse()? - StringWriter sWriter = new StringWriter(); - PrintWriter pWriter = new PrintWriter(sWriter); - pWriter.print("{"); - if (isXSRFEnabled(uI.getSession())) { - pWriter.print(getSecurityKeyUIDL(request)); - } - writeUidlResponse(request, true, pWriter, uI, false); - pWriter.print("}"); - String initialUIDL = sWriter.toString(); - getLogger().log(Level.FINE, "Initial UIDL:{0}", initialUIDL); - return initialUIDL; - } - - /** - * Serve a connector resource from the classpath if the resource has - * previously been registered by calling - * {@link #registerPublishedFile(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 servePublishedFile(VaadinRequest request, - VaadinResponse response) throws IOException { - - String pathInfo = request.getPathInfo(); - // + 2 to also remove beginning and ending slashes - String fileName = pathInfo - .substring(ApplicationConstants.PUBLISHED_FILE_PATH.length() + 2); - - final String mimetype = response.getService().getMimeType(fileName); - - // Security check: avoid accidentally serving from the UI of the - // classpath instead of relative to the context class - if (fileName.startsWith("/")) { - getLogger().log(Level.WARNING, - "Published file request starting with / rejected: {0}", - fileName); - response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); - return; - } - - // Check that the resource name has been registered - Class context; - synchronized (publishedFileContexts) { - context = publishedFileContexts.get(fileName); - } - - // Security check: don't serve resource if the name hasn't been - // registered in the map - if (context == null) { - getLogger() - .log(Level.WARNING, - "Rejecting published file request for file that has not been published: {0}", - fileName); - response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); - return; - } - - // Resolve file relative to the location of the context class - InputStream in = context.getResourceAsStream(fileName); - if (in == null) { - getLogger() - .log(Level.WARNING, - "{0} published by {1} not found. Verify that the file {2}/{3} is available on the classpath.", - new Object[] { - fileName, - context.getName(), - context.getPackage().getName() - .replace('.', '/'), fileName }); - response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); - 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 UI - * The UI for this request - * - * @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable) + * Handles an exception that occurred when processing RPC calls or a file + * upload. * - * @param request - * @param response - * @throws IOException - * @throws InvalidUIDLSecurityKeyException - */ - public void handleFileUpload(VaadinSession session, VaadinRequest request, - VaadinResponse response) throws IOException, - InvalidUIDLSecurityKeyException { - - /* - * URI pattern: APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] See - * #createReceiverUrl - */ - - String pathInfo = request.getPathInfo(); - // 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= UIid, 1 = cid, 2= name, 3 - // = sec key - String uiId = parts[0]; - String connectorId = parts[1]; - String variableName = parts[2]; - UI uI = session.getUIById(Integer.parseInt(uiId)); - UI.setCurrent(uI); - - StreamVariable streamVariable = uI.getConnectorTracker() - .getStreamVariable(connectorId, variableName); - String secKey = uI.getConnectorTracker().getSeckey(streamVariable); - if (secKey.equals(parts[3])) { - - ClientConnector source = getConnector(uI, 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!"); - } - - } - - /** - * Handles a heartbeat request. Heartbeat requests are periodically sent by - * the client-side to inform the server that the UI sending the heartbeat is - * still alive (the browser window is open, the connection is up) even when - * there are no UIDL requests for a prolonged period of time. UIs that do - * not receive either heartbeat or UIDL requests are eventually removed from - * the session and garbage collected. + * @deprecated As of 7.1. See #11411. * - * @param request - * @param response - * @param session - * @throws IOException + * @param ui + * The UI where the exception occured + * @param throwable + * The exception + * @param connector + * The Rpc target */ - public void handleHeartbeatRequest(VaadinRequest request, - VaadinResponse response, VaadinSession session) throws IOException { - UI ui = null; - try { - int uiId = Integer.parseInt(request - .getParameter(UIConstants.UI_ID_PARAMETER)); - ui = session.getUIById(uiId); - } catch (NumberFormatException nfe) { - // null-check below handles this as well - } - if (ui != null) { - ui.setLastHeartbeatTimestamp(System.currentTimeMillis()); - // Ensure that the browser does not cache heartbeat responses. - // iOS 6 Safari requires this (#10370) - response.setHeader("Cache-Control", "no-cache"); - } else { - response.sendError(HttpServletResponse.SC_NOT_FOUND, "UI not found"); - } + @Deprecated + public void handleConnectorRelatedException(ClientConnector connector, + Throwable throwable) { + ErrorEvent errorEvent = new ConnectorErrorEvent(connector, throwable); + ErrorHandler handler = ErrorEvent.findErrorHandler(connector); + handler.error(errorEvent); } /** - * Stream that extracts content from another stream until the boundary - * string is encountered. + * Requests that the given UI should be fully re-rendered on the client + * side. * - * Public only for unit tests, should be considered private for all other - * purposes. + * @since 7.1 + * @deprecated. As of 7.1. Should be refactored once locales are fixed + * (#11378) */ - 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(); - } - } - } + @Deprecated + public void repaintAll(UI ui) { + getClientCache(ui).clear(); + ui.getConnectorTracker().markAllConnectorsDirty(); + ui.getConnectorTracker().markAllClientSidesUninitialized(); - /** - * 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; - } + // Reset sent locales + resetLocales(); + requireLocale(session.getLocale().toString()); } private static final Logger getLogger() { diff --git a/server/src/com/vaadin/server/CommunicationManager.java b/server/src/com/vaadin/server/CommunicationManager.java index 8b3550481d..44e8f87d9f 100644 --- a/server/src/com/vaadin/server/CommunicationManager.java +++ b/server/src/com/vaadin/server/CommunicationManager.java @@ -80,7 +80,7 @@ public class CommunicationManager extends AbstractCommunicationManager { } @Override - protected InputStream getThemeResourceAsStream(UI uI, String themeName, + public InputStream getThemeResourceAsStream(UI uI, String themeName, String resource) { VaadinServletService service = (VaadinServletService) uI.getSession() .getService(); diff --git a/server/src/com/vaadin/server/ComponentSizeValidator.java b/server/src/com/vaadin/server/ComponentSizeValidator.java index f5e2e2fe12..27d087a2b2 100644 --- a/server/src/com/vaadin/server/ComponentSizeValidator.java +++ b/server/src/com/vaadin/server/ComponentSizeValidator.java @@ -191,7 +191,6 @@ public class ComponentSizeValidator implements Serializable { } public void reportErrors(PrintWriter clientJSON, - AbstractCommunicationManager communicationManager, PrintStream serverErrorStream) { clientJSON.write("{"); @@ -269,8 +268,7 @@ public class ComponentSizeValidator implements Serializable { } else { first = false; } - subError.reportErrors(clientJSON, communicationManager, - serverErrorStream); + subError.reportErrors(clientJSON, serverErrorStream); } clientJSON.write("]"); serverErrorStream.println("<< Sub erros"); diff --git a/server/src/com/vaadin/server/DragAndDropService.java b/server/src/com/vaadin/server/DragAndDropService.java index 5a54b5ae3a..e403f4d4cb 100644 --- a/server/src/com/vaadin/server/DragAndDropService.java +++ b/server/src/com/vaadin/server/DragAndDropService.java @@ -16,7 +16,7 @@ package com.vaadin.server; import java.io.IOException; -import java.io.PrintWriter; +import java.io.Writer; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -209,10 +209,10 @@ public class DragAndDropService implements VariableOwner, ClientConnector { return true; } - void printJSONResponse(PrintWriter outWriter) throws PaintException { + public void printJSONResponse(Writer outWriter) throws IOException { if (isDirty()) { - outWriter.print(", \"dd\":"); + outWriter.write(", \"dd\":"); JsonPaintTarget jsonPaintTarget = new JsonPaintTarget(manager, outWriter, false); diff --git a/server/src/com/vaadin/server/JsonPaintTarget.java b/server/src/com/vaadin/server/JsonPaintTarget.java index 11bfb33fe1..35ff8659ad 100644 --- a/server/src/com/vaadin/server/JsonPaintTarget.java +++ b/server/src/com/vaadin/server/JsonPaintTarget.java @@ -18,6 +18,7 @@ package com.vaadin.server; import java.io.PrintWriter; import java.io.Serializable; +import java.io.Writer; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -87,13 +88,12 @@ public class JsonPaintTarget implements PaintTarget { * if the paint operation failed. */ public JsonPaintTarget(AbstractCommunicationManager manager, - PrintWriter outWriter, boolean cachingRequired) - throws PaintException { + Writer outWriter, boolean cachingRequired) throws PaintException { this.manager = manager; // Sets the target for UIDL writing - uidlBuffer = outWriter; + uidlBuffer = new PrintWriter(outWriter); // Initialize tag-writing mOpenTags = new Stack(); @@ -1007,7 +1007,7 @@ public class JsonPaintTarget implements PaintTarget { return manager.getTagForType(clientConnectorClass); } - Collection> getUsedClientConnectors() { + public Collection> getUsedClientConnectors() { return usedClientConnectors; } diff --git a/server/src/com/vaadin/server/PortletCommunicationManager.java b/server/src/com/vaadin/server/PortletCommunicationManager.java index cece75847c..fdec421741 100644 --- a/server/src/com/vaadin/server/PortletCommunicationManager.java +++ b/server/src/com/vaadin/server/PortletCommunicationManager.java @@ -134,7 +134,7 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { } @Override - protected InputStream getThemeResourceAsStream(UI uI, String themeName, + public InputStream getThemeResourceAsStream(UI uI, String themeName, String resource) { VaadinPortletSession session = (VaadinPortletSession) uI.getSession(); PortletContext portletContext = session.getPortletSession() diff --git a/server/src/com/vaadin/server/ServerRpcManager.java b/server/src/com/vaadin/server/ServerRpcManager.java index ec25ce83ca..a1682cb453 100644 --- a/server/src/com/vaadin/server/ServerRpcManager.java +++ b/server/src/com/vaadin/server/ServerRpcManager.java @@ -139,7 +139,7 @@ public class ServerRpcManager implements Serializable { * * @return RPC interface type */ - protected Class getRpcInterface() { + public Class getRpcInterface() { return rpcInterface; } diff --git a/server/src/com/vaadin/server/ServletPortletHelper.java b/server/src/com/vaadin/server/ServletPortletHelper.java index ce9872f40e..baf697cae3 100644 --- a/server/src/com/vaadin/server/ServletPortletHelper.java +++ b/server/src/com/vaadin/server/ServletPortletHelper.java @@ -23,23 +23,14 @@ import com.vaadin.shared.ApplicationConstants; import com.vaadin.ui.Component; import com.vaadin.ui.UI; -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 +/** + * Contains helper methods shared by {@link VaadinServlet} and + * {@link VaadinPortlet}. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. + * @deprecated As of 7.1. Will be removed or refactored in the future. */ - -class ServletPortletHelper implements Serializable { +@Deprecated +public class ServletPortletHelper implements Serializable { public static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/"; /** * The default SystemMessages (read-only). diff --git a/server/src/com/vaadin/server/VaadinPortlet.java b/server/src/com/vaadin/server/VaadinPortlet.java index b4a2390fa5..2abf140a3d 100644 --- a/server/src/com/vaadin/server/VaadinPortlet.java +++ b/server/src/com/vaadin/server/VaadinPortlet.java @@ -24,7 +24,6 @@ import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.Method; import java.net.MalformedURLException; -import java.security.GeneralSecurityException; import java.util.Enumeration; import java.util.Map; import java.util.Properties; @@ -52,7 +51,12 @@ import javax.servlet.http.HttpServletResponse; import com.liferay.portal.kernel.util.PortalClassInvoker; import com.liferay.portal.kernel.util.PropsUtil; import com.vaadin.server.AbstractCommunicationManager.Callback; -import com.vaadin.ui.UI; +import com.vaadin.server.communication.FileUploadHandler; +import com.vaadin.server.communication.HeartbeatHandler; +import com.vaadin.server.communication.PortletListenerNotifier; +import com.vaadin.server.communication.PublishedFileHandler; +import com.vaadin.server.communication.UIInitHandler; +import com.vaadin.server.communication.UidlRequestHandler; import com.vaadin.util.CurrentInstance; /** @@ -422,86 +426,44 @@ public class VaadinPortlet extends GenericPortlet implements Constants, return; } - PortletCommunicationManager communicationManager = (PortletCommunicationManager) vaadinSession - .getCommunicationManager(); - if (requestType == RequestType.PUBLISHED_FILE) { - communicationManager.servePublishedFile(vaadinRequest, - vaadinResponse); + new PublishedFileHandler().handleRequest(vaadinSession, + vaadinRequest, vaadinResponse); return; } else if (requestType == RequestType.HEARTBEAT) { - communicationManager.handleHeartbeatRequest( - vaadinRequest, vaadinResponse, vaadinSession); + new HeartbeatHandler().handleRequest(vaadinSession, + vaadinRequest, vaadinResponse); return; } - /* Update browser information from request */ - vaadinSession.getBrowser().updateRequestDetails( - vaadinRequest); - - /* Notify listeners */ - - // Finds the right UI - UI uI = null; - if (requestType == RequestType.UIDL) { - uI = getService().findUI(vaadinRequest); - } - - // TODO Should this happen before or after the transaction - // starts? - if (request instanceof RenderRequest) { - vaadinSession.firePortletRenderRequest(uI, - (RenderRequest) request, - (RenderResponse) response); - } else if (request instanceof ActionRequest) { - vaadinSession.firePortletActionRequest(uI, - (ActionRequest) request, - (ActionResponse) response); - } else if (request instanceof EventRequest) { - vaadinSession.firePortletEventRequest(uI, - (EventRequest) request, - (EventResponse) response); - } else if (request instanceof ResourceRequest) { - vaadinSession.firePortletResourceRequest(uI, - (ResourceRequest) request, - (ResourceResponse) response); - } + // Notify listeners + new PortletListenerNotifier().handleRequest(vaadinSession, + vaadinRequest, vaadinResponse); /* Handle the request */ if (requestType == RequestType.FILE_UPLOAD) { - // UI is resolved in handleFileUpload by - // PortletCommunicationManager - communicationManager.handleFileUpload(vaadinSession, + new FileUploadHandler().handleRequest(vaadinSession, vaadinRequest, vaadinResponse); return; } else if (requestType == RequestType.BROWSER_DETAILS) { - communicationManager.handleBrowserDetailsRequest( - vaadinRequest, vaadinResponse, vaadinSession); + new UIInitHandler().handleRequest(vaadinSession, + vaadinRequest, vaadinResponse); return; } else if (requestType == RequestType.UIDL) { // Handles AJAX UIDL requests - communicationManager.handleUidlRequest(vaadinRequest, - vaadinResponse, portletWrapper, uI); + new UidlRequestHandler(portletWrapper).handleRequest( + vaadinSession, vaadinRequest, vaadinResponse); - // Ensure that the browser does not cache UIDL - // responses. - // iOS 6 Safari requires this (#9732) - response.setProperty("Cache-Control", "no-cache"); return; } else { handleOtherRequest(vaadinRequest, vaadinResponse, requestType, vaadinSession, - communicationManager); + vaadinSession.getCommunicationManager()); } } 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(vaadinRequest, vaadinResponse, vaadinSession, e); @@ -566,7 +528,7 @@ public class VaadinPortlet extends GenericPortlet implements Constants, private void handleOtherRequest(VaadinPortletRequest request, VaadinResponse response, RequestType requestType, VaadinSession vaadinSession, - PortletCommunicationManager communicationManager) + AbstractCommunicationManager communicationManager) throws PortletException, IOException, MalformedURLException { if (requestType == RequestType.APP || requestType == RequestType.RENDER) { if (!communicationManager.handleOtherRequest(request, response)) { diff --git a/server/src/com/vaadin/server/VaadinServlet.java b/server/src/com/vaadin/server/VaadinServlet.java index 11a7439c66..fefa8699e1 100644 --- a/server/src/com/vaadin/server/VaadinServlet.java +++ b/server/src/com/vaadin/server/VaadinServlet.java @@ -24,7 +24,6 @@ import java.io.PrintWriter; 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; @@ -35,15 +34,18 @@ 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 com.vaadin.sass.internal.ScssStylesheet; import com.vaadin.server.AbstractCommunicationManager.Callback; +import com.vaadin.server.communication.FileUploadHandler; +import com.vaadin.server.communication.HeartbeatHandler; +import com.vaadin.server.communication.PublishedFileHandler; +import com.vaadin.server.communication.UIInitHandler; +import com.vaadin.server.communication.UidlRequestHandler; import com.vaadin.shared.ApplicationConstants; -import com.vaadin.ui.UI; import com.vaadin.util.CurrentInstance; @SuppressWarnings("serial") @@ -285,49 +287,29 @@ public class VaadinServlet extends HttpServlet implements Constants { return; } - CommunicationManager communicationManager = (CommunicationManager) vaadinSession - .getCommunicationManager(); - if (requestType == RequestType.PUBLISHED_FILE) { - communicationManager.servePublishedFile(request, response); + new PublishedFileHandler().handleRequest(vaadinSession, + request, response); return; } else if (requestType == RequestType.HEARTBEAT) { - communicationManager.handleHeartbeatRequest(request, response, - vaadinSession); + new HeartbeatHandler().handleRequest(vaadinSession, request, + response); return; - } - - /* Update browser information from the request */ - vaadinSession.getBrowser().updateRequestDetails(request); - - /* Handle the request */ - if (requestType == RequestType.FILE_UPLOAD) { - // UI is resolved in communication manager - communicationManager.handleFileUpload(vaadinSession, request, + } else if (requestType == RequestType.FILE_UPLOAD) { + new FileUploadHandler().handleRequest(vaadinSession, request, response); return; } else if (requestType == RequestType.UIDL) { - UI uI = getService().findUI(request); - if (uI == null) { - throw new ServletException(ERROR_NO_UI_FOUND); - } - // Handles AJAX UIDL requests - communicationManager.handleUidlRequest(request, response, - servletWrapper, uI); - - // Ensure that the browser does not cache UIDL responses. - // iOS 6 Safari requires this (#9732) - response.setHeader("Cache-Control", "no-cache"); - + new UidlRequestHandler(servletWrapper).handleRequest( + vaadinSession, request, response); return; } else if (requestType == RequestType.BROWSER_DETAILS) { // Browser details - not related to a specific UI - communicationManager.handleBrowserDetailsRequest(request, - response, vaadinSession); + new UIInitHandler().handleRequest(vaadinSession, request, + response); return; - } - - if (communicationManager.handleOtherRequest(request, response)) { + } else if (vaadinSession.getCommunicationManager() + .handleOtherRequest(request, response)) { return; } @@ -337,8 +319,6 @@ public class VaadinServlet extends HttpServlet implements Constants { } 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, vaadinSession, e); } finally { @@ -442,7 +422,7 @@ public class VaadinServlet extends HttpServlet implements Constants { */ @Deprecated protected void criticalNotification(VaadinServletRequest request, - HttpServletResponse response, String caption, String message, + VaadinServletResponse response, String caption, String message, String details, String url) throws IOException { if (ServletPortletHelper.isUIDLRequest(request)) { @@ -493,9 +473,7 @@ public class VaadinServlet extends HttpServlet implements Constants { output += ""; } writeResponse(response, "text/html; charset=UTF-8", output); - } - } /** @@ -511,7 +489,7 @@ public class VaadinServlet extends HttpServlet implements Constants { private void writeResponse(HttpServletResponse response, String contentType, String output) throws IOException { response.setContentType(contentType); - final ServletOutputStream out = response.getOutputStream(); + final OutputStream out = response.getOutputStream(); // Set the response type final PrintWriter outWriter = new PrintWriter(new BufferedWriter( new OutputStreamWriter(out, "UTF-8"))); diff --git a/server/src/com/vaadin/server/WebBrowser.java b/server/src/com/vaadin/server/WebBrowser.java index 4122f053ae..28b92e28b8 100644 --- a/server/src/com/vaadin/server/WebBrowser.java +++ b/server/src/com/vaadin/server/WebBrowser.java @@ -413,7 +413,7 @@ public class WebBrowser implements Serializable { * @param request * the Vaadin request to read the information from */ - void updateRequestDetails(VaadinRequest request) { + public void updateRequestDetails(VaadinRequest request) { locale = request.getLocale(); address = request.getRemoteAddr(); secureConnection = request.isSecure(); diff --git a/server/src/com/vaadin/server/AbstractStreamingEvent.java b/server/src/com/vaadin/server/communication/AbstractStreamingEvent.java similarity index 95% rename from server/src/com/vaadin/server/AbstractStreamingEvent.java rename to server/src/com/vaadin/server/communication/AbstractStreamingEvent.java index b7bf4e042f..054bc14f2d 100644 --- a/server/src/com/vaadin/server/AbstractStreamingEvent.java +++ b/server/src/com/vaadin/server/communication/AbstractStreamingEvent.java @@ -13,8 +13,9 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; +import com.vaadin.server.StreamVariable; import com.vaadin.server.StreamVariable.StreamingEvent; /** diff --git a/server/src/com/vaadin/server/communication/ClientRpcWriter.java b/server/src/com/vaadin/server/communication/ClientRpcWriter.java new file mode 100644 index 0000000000..285adac7a5 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ClientRpcWriter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.ClientMethodInvocation; +import com.vaadin.server.EncodeResult; +import com.vaadin.server.JsonCodec; +import com.vaadin.server.PaintException; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.ui.UI; + +/** + * Serializes {@link ClientRpc client RPC} invocations to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ClientRpcWriter implements Serializable { + + /** + * Writes a JSON object containing all pending client RPC invocations in the + * given UI. + * + * @param ui + * The {@link UI} whose RPC calls to write. + * @param writer + * The {@link Writer} used to write the JSON. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer) throws IOException { + + Collection pendingInvocations = collectPendingRpcCalls(ui + .getConnectorTracker().getDirtyVisibleConnectors()); + + 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()); + // } + // } + EncodeResult encodeResult = JsonCodec.encode( + invocation.getParameters()[i], referenceParameter, + parameterType, ui.getConnectorTracker()); + paramJson.put(encodeResult.getEncodedValue()); + } + 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); + } + } + writer.write(rpcCalls.toString()); + } + + /** + * 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 Collection collectPendingRpcCalls( + Collection rpcPendingQueue) { + List pendingInvocations = new ArrayList(); + for (ClientConnector connector : rpcPendingQueue) { + List paintablePendingRpc = connector + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList( + 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) oldPendingRpc + .get(oldIndex)) + .compareTo(paintablePendingRpc + .get(paintableIndex)) <= 0)) { + pendingInvocations.add(oldPendingRpc.get(oldIndex++)); + } else { + pendingInvocations.add(paintablePendingRpc + .get(paintableIndex++)); + } + } + } + } + return pendingInvocations; + } +} diff --git a/server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java b/server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java new file mode 100644 index 0000000000..0d1370528d --- /dev/null +++ b/server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Collection; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.AbstractClientConnector; +import com.vaadin.server.AbstractCommunicationManager; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.PaintException; +import com.vaadin.ui.UI; + +/** + * Serializes a connector hierarchy to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ConnectorHierarchyWriter implements Serializable { + + /** + * Writes a JSON object containing the connector hierarchy (parent-child + * mappings) of the dirty connectors in the given UI. + * + * @param ui + * The {@link UI} whose hierarchy to write. + * @param writer + * The {@link Writer} used to write the JSON. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer) throws IOException { + + Collection dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + JSONObject hierarchyInfo = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorId = connector.getConnectorId(); + JSONArray children = new JSONArray(); + + for (ClientConnector child : AbstractClientConnector + .getAllChildrenIterable(connector)) { + if (AbstractCommunicationManager + .isConnectorVisibleToClient(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); + } + } + writer.write(hierarchyInfo.toString()); + } +} diff --git a/server/src/com/vaadin/server/communication/ConnectorTypeWriter.java b/server/src/com/vaadin/server/communication/ConnectorTypeWriter.java new file mode 100644 index 0000000000..eaa1c83ff2 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ConnectorTypeWriter.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Collection; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.ui.UI; + +/** + * Serializes connector type mappings to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ConnectorTypeWriter implements Serializable { + + /** + * Writes a JSON object containing connector-ID-to-type-ID mappings for each + * dirty Connector in the given UI. + * + * @param ui + * The {@link UI} containing dirty connectors + * @param writer + * The {@link Writer} used to write the JSON. + * @param target + * The paint target containing the connector type IDs. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer, PaintTarget target) + throws IOException { + + Collection dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + JSONObject connectorTypes = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorType = target.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); + } + } + writer.write(connectorTypes.toString()); + } +} diff --git a/server/src/com/vaadin/server/communication/FileUploadHandler.java b/server/src/com/vaadin/server/communication/FileUploadHandler.java new file mode 100644 index 0000000000..444e0c64cd --- /dev/null +++ b/server/src/com/vaadin/server/communication/FileUploadHandler.java @@ -0,0 +1,586 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.NoInputStreamException; +import com.vaadin.server.NoOutputStreamException; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.StreamVariable; +import com.vaadin.server.StreamVariable.StreamingEndEvent; +import com.vaadin.server.StreamVariable.StreamingErrorEvent; +import com.vaadin.server.UploadException; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.Component; +import com.vaadin.ui.UI; + +/** + * Handles a file upload request submitted via an Upload component. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class FileUploadHandler implements RequestHandler { + + /** + * 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 class UploadInterruptedException extends Exception { + public UploadInterruptedException() { + super("Upload interrupted by other thread"); + } + } + + private static final int LF = "\n".getBytes()[0]; + + private static final String CRLF = "\r\n"; + + private static final String UTF8 = "UTF-8"; + + private static final String DASHDASH = "--"; + + /* Same as in apache commons file upload library that was previously used. */ + private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + /* + * URI pattern: APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] See + * #createReceiverUrl + */ + + String pathInfo = request.getPathInfo(); + // 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= UIid, 1 = cid, 2= name, 3 + // = sec key + String uiId = parts[0]; + String connectorId = parts[1]; + String variableName = parts[2]; + UI uI = session.getUIById(Integer.parseInt(uiId)); + UI.setCurrent(uI); + + StreamVariable streamVariable = uI.getConnectorTracker() + .getStreamVariable(connectorId, variableName); + String secKey = uI.getConnectorTracker().getSeckey(streamVariable); + if (secKey.equals(parts[3])) { + + ClientConnector source = session.getCommunicationManager() + .getConnector(uI, connectorId); + String contentType = request.getContentType(); + if (contentType.contains("boundary")) { + // Multipart requests contain boundary string + doHandleSimpleMultipartFileUpload(session, 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(session, request, response, streamVariable, + variableName, source, request.getContentLength()); + } + } else { + // TODO Should rethink error handling + } + + return true; + } + + 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(VaadinSession session, + VaadinRequest request, VaadinResponse 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.getBytes(UTF8).length + CRLF.length()); + if (readLine.startsWith("Content-Disposition:") + && readLine.indexOf("filename=") > 0) { + rawfilename = readLine.replaceAll(".*filename=", ""); + char quote = rawfilename.charAt(0); + rawfilename = rawfilename.substring(1); + rawfilename = rawfilename.substring(0, + rawfilename.indexOf(quote)); + 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() + CRLF.length()); + + /* + * 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(session, + simpleMultiPartReader, streamVariable, filename, mimeType, + contentLength); + if (forgetVariable) { + cleanStreamVariable(owner, variableName); + } + } catch (Exception e) { + session.lock(); + try { + session.getCommunicationManager() + .handleConnectorRelatedException(owner, e); + } finally { + session.unlock(); + } + } + 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(VaadinSession session, + VaadinRequest request, VaadinResponse 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(session, stream, + streamVariable, filename, mimeType, contentLength); + if (forgetVariable) { + cleanStreamVariable(owner, variableName); + } + } catch (Exception e) { + session.lock(); + try { + session.getCommunicationManager() + .handleConnectorRelatedException(owner, e); + } finally { + session.unlock(); + } + } + 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(VaadinSession session, + 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"); + } + + OutputStream out = null; + int totalBytes = 0; + StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( + filename, type, contentLength); + try { + boolean listenProgress; + session.lock(); + try { + streamVariable.streamingStarted(startedEvent); + out = streamVariable.getOutputStream(); + listenProgress = streamVariable.listenProgress(); + } finally { + session.unlock(); + } + + // 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 + session.lock(); + try { + StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( + filename, type, contentLength, totalBytes); + streamVariable.onProgress(progressEvent); + } finally { + session.unlock(); + } + } + if (streamVariable.isInterrupted()) { + throw new UploadInterruptedException(); + } + } + + // upload successful + out.close(); + StreamingEndEvent event = new StreamingEndEventImpl(filename, type, + totalBytes); + session.lock(); + try { + streamVariable.streamingFinished(event); + } finally { + session.unlock(); + } + + } catch (UploadInterruptedException e) { + // Download interrupted by application code + tryToCloseStream(out); + StreamingErrorEvent event = new StreamingErrorEventImpl(filename, + type, contentLength, totalBytes, e); + session.lock(); + try { + streamVariable.streamingFailed(event); + } finally { + session.unlock(); + } + // 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); + session.lock(); + try { + StreamingErrorEvent event = new StreamingErrorEventImpl( + filename, type, contentLength, totalBytes, e); + streamVariable.streamingFailed(event); + // throw exception for terminal to be handled (to be passed to + // terminalErrorHandler) + throw new UploadException(e); + } finally { + session.unlock(); + } + } + 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(VaadinRequest request, + VaadinResponse 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("download handled"); + outWriter.flush(); + out.close(); + } + + private void cleanStreamVariable(ClientConnector owner, String name) { + owner.getUI().getConnectorTracker() + .cleanStreamVariable(owner.getConnectorId(), name); + } +} diff --git a/server/src/com/vaadin/server/communication/HeartbeatHandler.java b/server/src/com/vaadin/server/communication/HeartbeatHandler.java new file mode 100644 index 0000000000..75d4f870c1 --- /dev/null +++ b/server/src/com/vaadin/server/communication/HeartbeatHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.ui.UIConstants; +import com.vaadin.ui.UI; + +/** + * Handles heartbeat requests. Heartbeat requests are periodically sent by the + * client-side to inform the server that the UI sending the heartbeat is still + * alive (the browser window is open, the connection is up) even when there are + * no UIDL requests for a prolonged period of time. UIs that do not receive + * either heartbeat or UIDL requests are eventually removed from the session and + * garbage collected. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class HeartbeatHandler implements RequestHandler { + + /** + * Handles a heartbeat request for the given session. Reads the GET + * parameter named {@link UIConstants#UI_ID_PARAMETER} to identify the UI. + * If the UI is found in the session, sets it + * {@link UI#getLastHeartbeatTimestamp() heartbeat timestamp} to the current + * time. Otherwise, writes a HTTP Not Found error to the response. + */ + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + UI ui = null; + try { + int uiId = Integer.parseInt(request + .getParameter(UIConstants.UI_ID_PARAMETER)); + ui = session.getUIById(uiId); + } catch (NumberFormatException nfe) { + // null-check below handles this as well + } + if (ui != null) { + ui.setLastHeartbeatTimestamp(System.currentTimeMillis()); + // Ensure that the browser does not cache heartbeat responses. + // iOS 6 Safari requires this (#10370) + response.setHeader("Cache-Control", "no-cache"); + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "UI not found"); + } + + return true; + } +} diff --git a/server/src/com/vaadin/server/communication/LegacyUidlWriter.java b/server/src/com/vaadin/server/communication/LegacyUidlWriter.java new file mode 100644 index 0000000000..ad99a2d8b5 --- /dev/null +++ b/server/src/com/vaadin/server/communication/LegacyUidlWriter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Logger; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.LegacyPaint; +import com.vaadin.server.PaintTarget; +import com.vaadin.ui.Component; +import com.vaadin.ui.LegacyComponent; +import com.vaadin.ui.UI; + +/** + * Serializes legacy UIDL changes to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class LegacyUidlWriter implements Serializable { + + /** + * Writes a JSON array containing the changes of all dirty + * {@link LegacyComponent}s in the given UI. + * + * @param ui + * The {@link UI} whose legacy changes to write + * @param writer + * The {@link Writer} to write the JSON with + * @param target + * The {@link PaintTarget} to use + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer, PaintTarget target) + throws IOException { + + Collection dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + List legacyComponents = new ArrayList(); + for (ClientConnector connector : dirtyVisibleConnectors) { + // All Components that want to use paintContent must implement + // LegacyComponent + if (connector instanceof LegacyComponent) { + legacyComponents.add((Component) connector); + } + } + sortByHierarchy(legacyComponents); + + writer.write("["); + for (Component c : legacyComponents) { + getLogger().fine( + "Painting LegacyComponent " + c.getClass().getName() + "@" + + Integer.toHexString(c.hashCode())); + target.startTag("change"); + final String pid = c.getConnectorId(); + target.addAttribute("pid", pid); + LegacyPaint.paint(c, target); + target.endTag("change"); + } + writer.write("]"); + } + + private void sortByHierarchy(List 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() { + @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 static final Logger getLogger() { + return Logger.getLogger(LegacyUidlWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/LocaleWriter.java b/server/src/com/vaadin/server/communication/LocaleWriter.java new file mode 100644 index 0000000000..c05649da19 --- /dev/null +++ b/server/src/com/vaadin/server/communication/LocaleWriter.java @@ -0,0 +1,204 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; + +/** + * Serializes locale information to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + * @deprecated See ticket + * #11378. + */ +@Deprecated +public class LocaleWriter implements Serializable { + + /** + * Writes a JSON object containing localized strings of the given locales. + * + * @param locales + * The list of {@link Locale}s to write. + * @param writer + * The {@link Writer} used to write the JSON. + * @throws IOException + * If the serialization fails. + * + */ + public void write(List locales, Writer writer) throws IOException { + + // Send locale informations to client + writer.write("["); + // TODO locales are currently sent on each request; this will be fixed + // by implementing #11378. + for (int pendingLocalesIndex = 0; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { + + final Locale l = generateLocale(locales.get(pendingLocalesIndex)); + // Locale name + writer.write("{\"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(); + writer.write("\"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] + "\"" + + "],"); + writer.write("\"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(); + writer.write("\"sdn\":[\"" + + // ShortDayNames + short_days[1] + "\",\"" + short_days[2] + "\",\"" + + short_days[3] + "\",\"" + short_days[4] + "\",\"" + + short_days[5] + "\",\"" + short_days[6] + "\",\"" + + short_days[7] + "\"" + "],"); + writer.write("\"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); + writer.write("\"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); + } + + writer.write("\"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 + "\","); + writer.write("\"thc\":" + twelve_hour_clock + ","); + writer.write("\"hmd\":\"" + hour_min_delimiter + "\""); + if (twelve_hour_clock) { + final String[] ampm = dfs.getAmPmStrings(); + writer.write(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] + + "\"]"); + } + writer.write("}"); + if (pendingLocalesIndex < locales.size() - 1) { + writer.write(","); + } + } + writer.write("]"); // Close locales + } + + /** + * 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]); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(LocaleWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/MetadataWriter.java b/server/src/com/vaadin/server/communication/MetadataWriter.java new file mode 100644 index 0000000000..7119e0ffeb --- /dev/null +++ b/server/src/com/vaadin/server/communication/MetadataWriter.java @@ -0,0 +1,137 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.Writer; +import java.util.List; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.ComponentSizeValidator; +import com.vaadin.server.ComponentSizeValidator.InvalidLayout; +import com.vaadin.server.SystemMessages; +import com.vaadin.ui.UI; +import com.vaadin.ui.Window; + +/** + * Serializes miscellaneous metadata to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class MetadataWriter implements Serializable { + + private int timeoutInterval = -1; + + /** + * Writes a JSON object containing metadata related to the given UI. + * + * @param ui + * The UI whose metadata to write. + * @param writer + * The writer used. + * @param repaintAll + * Whether the client should repaint everything. + * @param analyzeLayouts + * Whether detected layout problems should be reported in client + * and server console. + * @param hilightedConnector + * The connector that should be highlighted on the client or null + * if none. + * @param messages + * a {@link SystemMessages} containing client-side error + * messages. + * @throws IOException + * If the serialization fails. + * + */ + public void write(UI ui, Writer writer, boolean repaintAll, + boolean analyzeLayouts, ClientConnector hilightedConnector, + SystemMessages messages) throws IOException { + + List invalidComponentRelativeSizes = null; + + if (analyzeLayouts) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes(ui.getContent(), null, null); + + // Also check any existing subwindows + if (ui.getWindows() != null) { + for (Window subWindow : ui.getWindows()) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes( + subWindow.getContent(), + invalidComponentRelativeSizes, null); + } + } + } + + writer.write("{"); + + boolean metaOpen = false; + if (repaintAll) { + metaOpen = true; + writer.write("\"repaintAll\":true"); + if (analyzeLayouts) { + writer.write(", \"invalidLayouts\":"); + writer.write("["); + if (invalidComponentRelativeSizes != null) { + boolean first = true; + for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { + if (!first) { + writer.write(","); + } else { + first = false; + } + invalidLayout.reportErrors(new PrintWriter(writer), + System.err); + } + } + writer.write("]"); + } + if (hilightedConnector != null) { + writer.write(", \"hl\":\""); + writer.write(hilightedConnector.getConnectorId()); + writer.write("\""); + } + } + + // meta instruction for client to enable auto-forward to + // sessionExpiredURL after timer expires. + if (messages != null && messages.getSessionExpiredMessage() == null + && messages.getSessionExpiredCaption() == null + && messages.isSessionExpiredNotificationEnabled()) { + int newTimeoutInterval = ui.getSession().getSession() + .getMaxInactiveInterval(); + if (repaintAll || (timeoutInterval != newTimeoutInterval)) { + String escapedURL = messages.getSessionExpiredURL() == null ? "" + : messages.getSessionExpiredURL().replace("/", "\\/"); + if (metaOpen) { + writer.write(","); + } + writer.write("\"timedRedirect\":{\"interval\":" + + (newTimeoutInterval + 15) + ",\"url\":\"" + + escapedURL + "\"}"); + metaOpen = true; + } + timeoutInterval = newTimeoutInterval; + } + writer.write("}"); + } +} diff --git a/server/src/com/vaadin/server/communication/PortletListenerNotifier.java b/server/src/com/vaadin/server/communication/PortletListenerNotifier.java new file mode 100644 index 0000000000..aff5c8a80e --- /dev/null +++ b/server/src/com/vaadin/server/communication/PortletListenerNotifier.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.VaadinPortletRequest; +import com.vaadin.server.VaadinPortletResponse; +import com.vaadin.server.VaadinPortletSession; +import com.vaadin.server.VaadinPortletSession.PortletListener; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.UI; + +/** + * Notifies {@link PortletListener}s of a received portlet request. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PortletListenerNotifier implements RequestHandler { + + /** + * Fires portlet request events to any {@link PortletListener}s registered + * to the given session using + * {@link VaadinPortletSession#addPortletListener(PortletListener)}. The + * PortletListener method corresponding to the request type is invoked. + */ + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + VaadinPortletSession sess = (VaadinPortletSession) session; + PortletRequest req = ((VaadinPortletRequest) request) + .getPortletRequest(); + PortletResponse resp = ((VaadinPortletResponse) response) + .getPortletResponse(); + + // Finds the right UI + UI uI = null; + if (ServletPortletHelper.isUIDLRequest(request)) { + uI = session.getService().findUI(request); + } + + if (request instanceof RenderRequest) { + sess.firePortletRenderRequest(uI, (RenderRequest) req, + (RenderResponse) resp); + } else if (request instanceof ActionRequest) { + sess.firePortletActionRequest(uI, (ActionRequest) req, + (ActionResponse) resp); + } else if (request instanceof EventRequest) { + sess.firePortletEventRequest(uI, (EventRequest) req, + (EventResponse) resp); + } else if (request instanceof ResourceRequest) { + sess.firePortletResourceRequest(uI, (ResourceRequest) req, + (ResourceResponse) resp); + } + + return false; + } +} diff --git a/server/src/com/vaadin/server/communication/PublishedFileHandler.java b/server/src/com/vaadin/server/communication/PublishedFileHandler.java new file mode 100644 index 0000000000..3261dc7b9f --- /dev/null +++ b/server/src/com/vaadin/server/communication/PublishedFileHandler.java @@ -0,0 +1,145 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.annotations.JavaScript; +import com.vaadin.annotations.StyleSheet; +import com.vaadin.server.AbstractCommunicationManager; +import com.vaadin.server.Constants; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; + +/** + * Serves a connector resource from the classpath if the resource has previously + * been registered by calling + * {@link AbstractCommunicationManager#registerDependency(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. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PublishedFileHandler implements RequestHandler { + + /** + * Writes the connector resource identified by the request URI to the + * response. If a published resource corresponding to the URI path is not + * found, writes a HTTP Not Found error to the response. + */ + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + String pathInfo = request.getPathInfo(); + // + 2 to also remove beginning and ending slashes + String fileName = pathInfo + .substring(ApplicationConstants.PUBLISHED_FILE_PATH.length() + 2); + + final String mimetype = response.getService().getMimeType(fileName); + + // Security check: avoid accidentally serving from the UI of the + // classpath instead of relative to the context class + if (fileName.startsWith("/")) { + getLogger().warning( + "Published file request starting with / rejected: " + + fileName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); + return true; + } + + // Check that the resource name has been registered + // TODO PUSH refactor - is the synchronization correct at all? + Class context; + synchronized (session.getCommunicationManager().getDependencies()) { + context = session.getCommunicationManager().getDependencies() + .get(fileName); + } + + // Security check: don't serve resource if the name hasn't been + // registered in the map + if (context == null) { + getLogger().warning( + "Rejecting published file request for file that has not been published: " + + fileName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); + return true; + } + + // Resolve file relative to the location of the context class + InputStream in = context.getResourceAsStream(fileName); + if (in == null) { + getLogger().warning( + fileName + " published by " + context.getName() + + " not found. Verify that the file " + + context.getPackage().getName().replace('.', '/') + + '/' + fileName + + " is available on the classpath."); + response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); + return true; + } + + // 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 + } + } + } + + return true; + } + + private static final Logger getLogger() { + return Logger.getLogger(PublishedFileHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/ResourceWriter.java b/server/src/com/vaadin/server/communication/ResourceWriter.java new file mode 100644 index 0000000000..3ba3f3f598 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ResourceWriter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.io.Writer; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.server.AbstractCommunicationManager; +import com.vaadin.server.JsonPaintTarget; +import com.vaadin.ui.CustomLayout; +import com.vaadin.ui.UI; + +/** + * Serializes resources to JSON. Currently only used for {@link CustomLayout} + * templates. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ResourceWriter implements Serializable { + + /** + * Writes a JSON object containing registered resources. + * + * @param ui + * The {@link UI} whose resources to write. + * @param writer + * The {@link Writer} to use. + * @param target + * The {@link JsonPaintTarget} containing the resources. + * @throws IOException + */ + public void write(UI ui, Writer writer, JsonPaintTarget target) + throws IOException { + + // TODO PUSH Refactor so that this is not needed + AbstractCommunicationManager manager = ui.getSession() + .getCommunicationManager(); + + // Precache custom layouts + + // TODO We should only precache the layouts that are not + // cached already (plagiate from usedPaintableTypes) + + writer.write("{"); + int resourceIndex = 0; + for (final Iterator i = target.getUsedResources().iterator(); i + .hasNext();) { + final String resource = (String) i.next(); + InputStream is = null; + try { + is = manager.getThemeResourceAsStream(ui, manager.getTheme(ui), + resource); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Failed to get theme resource stream.", e); + } + if (is != null) { + + writer.write((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); + } + writer.write("\"" + + JsonPaintTarget.escapeJSON(layout.toString()) + "\""); + } else { + // FIXME: Handle exception + getLogger().severe("CustomLayout not found: " + resource); + } + } + writer.write("}"); + } + + private static final Logger getLogger() { + return Logger.getLogger(ResourceWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/ServerRpcHandler.java b/server/src/com/vaadin/server/communication/ServerRpcHandler.java new file mode 100644 index 0000000000..8d33ea8f85 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ServerRpcHandler.java @@ -0,0 +1,494 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Reader; +import java.io.Serializable; +import java.lang.reflect.Type; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.server.AbstractCommunicationManager; +import com.vaadin.server.AbstractCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.JsonCodec; +import com.vaadin.server.ServerRpcManager; +import com.vaadin.server.ServerRpcManager.RpcInvocationException; +import com.vaadin.server.ServerRpcMethodInvocation; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VariableOwner; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.UI; + +/** + * Handles a client-to-server message containing serialized {@link ServerRpc + * server RPC} invocations. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ServerRpcHandler implements Serializable { + + /* Variable records indexes */ + public static final char VAR_BURST_SEPARATOR = '\u001d'; + + public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + // 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"; + + /** + * Reads JSON containing zero or more serialized RPC calls (including legacy + * variable changes) and executes the calls. + * + * @param ui + * The {@link UI} receiving the calls. + * @param reader + * The {@link Reader} used to read the JSON. + * @param request + * @throws IOException + * If reading the message fails. + * @throws InvalidUIDLSecurityKeyException + * If the received security key does not match the one stored in + * the session. + * @throws JSONException + * If deserializing the JSON fails. + */ + public void handleRpc(UI ui, Reader reader, VaadinRequest request) + throws IOException, InvalidUIDLSecurityKeyException, JSONException { + + // Verify that there's an UI + if (ui == null) { + // This should not happen, no windows exists but + // session is still open. + getLogger().warning("Could not get UI for session"); + return; + } + + ui.getSession().setLastRequestTimestamp(System.currentTimeMillis()); + + // Change all variables based on request parameters + handleVariables(ui, reader, request); + } + + private void handleVariables(UI uI, Reader reader, VaadinRequest request) + throws IOException, InvalidUIDLSecurityKeyException, JSONException { + + String changes = getMessage(reader); + + final String[] bursts = changes.split(String + .valueOf(VAR_BURST_SEPARATOR)); + + if (bursts.length > 2) { + throw new RuntimeException( + "Multiple variable bursts not supported in Vaadin 7"); + } else if (bursts.length <= 2) { + // The client sometimes sends empty messages, this is probably a bug + return; + } + + // Security: double cookie submission pattern unless disabled by + // property + if (uI.getSession().getConfiguration().isXsrfProtectionEnabled()) { + if (bursts.length == 1 && "init".equals(bursts[0])) { + // init request; don't handle any variables, key sent in + // response. + // TODO This seems to be dead code + request.setAttribute( + AbstractCommunicationManager.WRITE_SECURITY_TOKEN_FLAG, + true); + return; + } else { + // ApplicationServlet has stored the security token in the + // session; check that it matched the one sent in the UIDL + String sessId = (String) uI + .getSession() + .getSession() + .getAttribute( + ApplicationConstants.UIDL_SECURITY_TOKEN_ID); + + if (sessId == null || !sessId.equals(bursts[0])) { + throw new InvalidUIDLSecurityKeyException(""); + } + } + + } + handleBurst(uI, unescapeBurst(bursts[1])); + } + + /** + * 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 uI + * the UI receiving the burst + * @param burst + * the content of the burst as a String to be parsed + */ + private void handleBurst(UI uI, String burst) { + // TODO PUSH Refactor so that this is not needed + AbstractCommunicationManager manager = uI.getSession() + .getCommunicationManager(); + + try { + Set enabledConnectors = new HashSet(); + + List invocations = parseInvocations( + uI.getConnectorTracker(), burst); + for (MethodInvocation invocation : invocations) { + final ClientConnector connector = manager.getConnector(uI, + 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 = manager.getConnector(uI, + invocation.getConnectorId()); + if (connector == null) { + getLogger() + .log(Level.WARNING, + "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})", + new Object[] { invocation.getConnectorId(), + invocation.getInterfaceName(), + invocation.getMethodName() }); + 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 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) { + manager.handleConnectorRelatedException(connector, e); + } + } else { + + // All code below is for legacy variable changes + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + Map changes = legacyInvocation + .getVariableChanges(); + try { + if (connector instanceof VariableOwner) { + // The source parameter is never used anywhere + changeVariables(null, (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) { + manager.handleConnectorRelatedException(connector, e); + } + } + } + } catch (JSONException e) { + getLogger().warning( + "Unable to parse RPC call from the client: " + + e.getMessage()); + throw new RuntimeException(e); + } + } + + /** + * 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 parseInvocations( + ConnectorTracker connectorTracker, String burst) + throws JSONException { + JSONArray invocationsJson = new JSONArray(burst); + + ArrayList invocations = new ArrayList(); + + 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 if the invocation was a legacy invocation and it + // was merged with the previous one or if the invocation was + // rejected because of an error. + 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); + + if (connectorTracker.getConnector(connectorId) == null + && !connectorId + .equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) { + getLogger() + .log(Level.WARNING, + "RPC call to " + + interfaceName + + "." + + methodName + + " received for connector " + + connectorId + + " but no such connector could be found. Resynchronizing client."); + // This is likely an out of sync issue (client tries to update a + // connector which is not present). Force resync. + connectorTracker.markAllConnectorsDirty(); + return null; + } + + 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 { + ClientConnector connector = connectorTracker.getConnector(connectorId); + + ServerRpcManager rpcManager = connector.getRpcManager(interfaceName); + if (rpcManager == null) { + /* + * Security: Don't even decode the json parameters if no RpcManager + * corresponding to the received method invocation has been + * registered. + */ + getLogger().warning( + "Ignoring RPC call to " + interfaceName + "." + methodName + + " in connector " + connector.getClass().getName() + + "(" + connectorId + + ") as no RPC implementation is regsitered"); + return null; + } + + // Use interface from RpcManager instead of loading the class based on + // the string name to avoid problems with OSGi + Class rpcInterface = rpcManager.getRpcInterface(); + + ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( + connectorId, rpcInterface, 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, VariableOwner owner, + Map m) { + owner.changeVariables(source, m); + } + + /** + * 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(); + } + + protected String getMessage(Reader reader) throws IOException { + + StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE); + char[] buffer = new char[MAX_BUFFER_SIZE]; + + while (true) { + int read = reader.read(buffer); + if (read == -1) { + break; + } + sb.append(buffer, 0, read); + } + + return sb.toString(); + } + + private static final Logger getLogger() { + return Logger.getLogger(ServerRpcHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/SharedStateWriter.java b/server/src/com/vaadin/server/communication/SharedStateWriter.java new file mode 100644 index 0000000000..fdf834387f --- /dev/null +++ b/server/src/com/vaadin/server/communication/SharedStateWriter.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Collection; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.PaintException; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.ui.UI; + +/** + * Serializes {@link SharedState shared state} changes to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class SharedStateWriter implements Serializable { + + /** + * Writes a JSON object containing the pending state changes of the dirty + * connectors of the given UI. + * + * @param ui + * The UI whose state changes should be written. + * @param writer + * The writer to use. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer) throws IOException { + + Collection dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + JSONObject sharedStates = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + // encode and send shared state + try { + JSONObject stateJson = connector.encodeState(); + + if (stateJson != null && stateJson.length() != 0) { + 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); + } + } + writer.write(sharedStates.toString()); + } +} diff --git a/server/src/com/vaadin/server/StreamingEndEventImpl.java b/server/src/com/vaadin/server/communication/StreamingEndEventImpl.java similarity index 91% rename from server/src/com/vaadin/server/StreamingEndEventImpl.java rename to server/src/com/vaadin/server/communication/StreamingEndEventImpl.java index 756cadee6b..e241bbbfaf 100644 --- a/server/src/com/vaadin/server/StreamingEndEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingEndEventImpl.java @@ -13,8 +13,9 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; +import com.vaadin.server.StreamVariable; import com.vaadin.server.StreamVariable.StreamingEndEvent; @SuppressWarnings("serial") diff --git a/server/src/com/vaadin/server/StreamingErrorEventImpl.java b/server/src/com/vaadin/server/communication/StreamingErrorEventImpl.java similarity index 93% rename from server/src/com/vaadin/server/StreamingErrorEventImpl.java rename to server/src/com/vaadin/server/communication/StreamingErrorEventImpl.java index 53e25399cd..1ee74c68b6 100644 --- a/server/src/com/vaadin/server/StreamingErrorEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingErrorEventImpl.java @@ -13,8 +13,9 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; +import com.vaadin.server.StreamVariable; import com.vaadin.server.StreamVariable.StreamingErrorEvent; @SuppressWarnings("serial") diff --git a/server/src/com/vaadin/server/StreamingProgressEventImpl.java b/server/src/com/vaadin/server/communication/StreamingProgressEventImpl.java similarity index 92% rename from server/src/com/vaadin/server/StreamingProgressEventImpl.java rename to server/src/com/vaadin/server/communication/StreamingProgressEventImpl.java index 610cd30c13..c07e37e196 100644 --- a/server/src/com/vaadin/server/StreamingProgressEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingProgressEventImpl.java @@ -13,8 +13,9 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; +import com.vaadin.server.StreamVariable; import com.vaadin.server.StreamVariable.StreamingProgressEvent; @SuppressWarnings("serial") diff --git a/server/src/com/vaadin/server/StreamingStartEventImpl.java b/server/src/com/vaadin/server/communication/StreamingStartEventImpl.java similarity index 93% rename from server/src/com/vaadin/server/StreamingStartEventImpl.java rename to server/src/com/vaadin/server/communication/StreamingStartEventImpl.java index 3cd41bbb6d..a7f13be499 100644 --- a/server/src/com/vaadin/server/StreamingStartEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingStartEventImpl.java @@ -13,8 +13,9 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; +import com.vaadin.server.StreamVariable; import com.vaadin.server.StreamVariable.StreamingStartEvent; @SuppressWarnings("serial") diff --git a/server/src/com/vaadin/server/communication/UIInitHandler.java b/server/src/com/vaadin/server/communication/UIInitHandler.java new file mode 100644 index 0000000000..aea8bbd5cd --- /dev/null +++ b/server/src/com/vaadin/server/communication/UIInitHandler.java @@ -0,0 +1,249 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.annotations.PreserveOnRefresh; +import com.vaadin.server.LegacyApplicationUIProvider; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.UIClassSelectionEvent; +import com.vaadin.server.UICreateEvent; +import com.vaadin.server.UIProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.ui.UIConstants; +import com.vaadin.ui.UI; + +/** + * Handles an initial request from the client to initialize a {@link UI}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class UIInitHandler implements RequestHandler { + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + // 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"))); + + session.lock(); + try { + assert UI.getCurrent() == null; + + // Set browser information from the request + session.getBrowser().updateRequestDetails(request); + + response.setContentType("application/json; charset=UTF-8"); + + UI uI = getBrowserDetailsUI(request, session); + + session.getCommunicationManager().repaintAll(uI); + + JSONObject params = new JSONObject(); + params.put(UIConstants.UI_ID_PARAMETER, uI.getUIId()); + String initialUIDL = getInitialUidl(request, uI); + params.put("uidl", initialUIDL); + + outWriter.write(params.toString()); + // NOTE GateIn requires the buffers to be flushed to work + outWriter.flush(); + out.flush(); + } catch (JSONException e) { + // TODO PUSH handle + e.printStackTrace(); + } finally { + session.unlock(); + outWriter.close(); + } + + return true; + } + + private UI getBrowserDetailsUI(VaadinRequest request, VaadinSession session) { + VaadinService vaadinService = request.getService(); + + List uiProviders = session.getUIProviders(); + + UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent( + request); + + UIProvider provider = null; + Class uiClass = null; + for (UIProvider p : uiProviders) { + // Check for existing LegacyWindow + if (p instanceof LegacyApplicationUIProvider) { + LegacyApplicationUIProvider legacyProvider = (LegacyApplicationUIProvider) p; + + UI existingUi = legacyProvider + .getExistingUI(classSelectionEvent); + if (existingUi != null) { + reinitUI(existingUi, request); + return existingUi; + } + } + + uiClass = p.getUIClass(classSelectionEvent); + if (uiClass != null) { + provider = p; + break; + } + } + + if (provider == null || uiClass == null) { + return null; + } + + // Check for an existing UI based on window.name + + // Special parameter sent by vaadinBootstrap.js + String windowName = request.getParameter("v-wn"); + + Map retainOnRefreshUIs = session + .getPreserveOnRefreshUIs(); + if (windowName != null && !retainOnRefreshUIs.isEmpty()) { + // Check for a known UI + + Integer retainedUIId = retainOnRefreshUIs.get(windowName); + + if (retainedUIId != null) { + UI retainedUI = session.getUIById(retainedUIId.intValue()); + if (uiClass.isInstance(retainedUI)) { + reinitUI(retainedUI, request); + return retainedUI; + } else { + getLogger().info( + "Not using retained UI in " + windowName + + " because retained UI was of type " + + retainedUI.getClass() + " but " + uiClass + + " is expected for the request."); + } + } + } + + // No existing UI found - go on by creating and initializing one + + Integer uiId = Integer.valueOf(session.getNextUIid()); + + // Explicit Class.cast to detect if the UIProvider does something + // unexpected + UICreateEvent event = new UICreateEvent(request, uiClass, uiId); + UI ui = uiClass.cast(provider.createInstance(event)); + + // Initialize some fields for a newly created UI + if (ui.getSession() != session) { + // Session already set for LegacyWindow + ui.setSession(session); + } + + // Set thread local here so it is available in init + UI.setCurrent(ui); + + ui.doInit(request, uiId.intValue()); + + session.addUI(ui); + + // Remember if it should be remembered + if (vaadinService.preserveUIOnRefresh(provider, event)) { + // Remember this UI + if (windowName == null) { + getLogger().warning( + "There is no window.name available for UI " + uiClass + + " that should be preserved."); + } else { + session.getPreserveOnRefreshUIs().put(windowName, uiId); + } + } + + return ui; + } + + /** + * Updates a UI that has already been initialized but is now loaded again, + * e.g. because of {@link PreserveOnRefresh}. + * + * @param ui + * @param request + */ + private void reinitUI(UI ui, VaadinRequest request) { + UI.setCurrent(ui); + + // Fire fragment change if the fragment has changed + String location = request.getParameter("v-loc"); + if (location != null) { + ui.getPage().updateLocation(location); + } + } + + /** + * 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 uI + * the UI for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws JSONException + * if an exception occurs while encoding output + * @throws IOException + */ + protected String getInitialUidl(VaadinRequest request, UI uI) + throws JSONException, IOException { + StringWriter writer = new StringWriter(); + try { + writer.write("{"); + if (uI.getSession().getConfiguration().isXsrfProtectionEnabled()) { + writer.write(uI.getSession().getCommunicationManager() + .getSecurityKeyUIDL(request)); + } + new UidlWriter().write(uI, writer, true, false); + writer.write("}"); + + String initialUIDL = writer.toString(); + getLogger().log(Level.FINE, "Initial UIDL:" + initialUIDL); + return initialUIDL; + } finally { + writer.close(); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(UIInitHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java new file mode 100644 index 0000000000..c200abb31d --- /dev/null +++ b/server/src/com/vaadin/server/communication/UidlRequestHandler.java @@ -0,0 +1,274 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONException; + +import com.vaadin.server.AbstractCommunicationManager; +import com.vaadin.server.AbstractCommunicationManager.Callback; +import com.vaadin.server.AbstractCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.Constants; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.Version; +import com.vaadin.ui.Component; +import com.vaadin.ui.UI; + +/** + * Processes a UIDL request from the client. + * + * Uses {@link ServerRpcHandler} to execute client-to-server RPC invocations and + * {@link UidlWriter} to write state changes and client RPC calls back to the + * client. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class UidlRequestHandler implements RequestHandler { + + private Callback criticalNotifier; + + private ServerRpcHandler rpcHandler = new ServerRpcHandler(); + + public UidlRequestHandler(Callback criticalNotifier) { + this.criticalNotifier = criticalNotifier; + } + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + UI uI = session.getService().findUI(request); + + checkWidgetsetVersion(request); + String requestThemeName = request.getParameter("theme"); + ClientConnector highlightedConnector; + // repaint requested or session has timed out and new one is created + boolean repaintAll; + final OutputStream out = response.getOutputStream(); + + // TODO PUSH repaintAll, analyzeLayouts, highlightConnector should be + // part of the message payload to make the functionality transport + // agnostic + + repaintAll = (request + .getParameter(ApplicationConstants.URL_PARAMETER_REPAINT_ALL) != null); + + boolean analyzeLayouts = false; + if (repaintAll) { + // analyzing can be done only with repaintAll + analyzeLayouts = (request + .getParameter(ApplicationConstants.PARAM_ANALYZE_LAYOUTS) != null); + + String pid = request + .getParameter(ApplicationConstants.PARAM_HIGHLIGHT_CONNECTOR); + if (pid != null) { + highlightedConnector = uI.getConnectorTracker().getConnector( + pid); + highlightConnector(highlightedConnector); + } + } + + final Writer outWriter = new BufferedWriter(new OutputStreamWriter(out, + "UTF-8")); + + // The rest of the process is synchronized with the session + // in order to guarantee that no parallel variable handling is + // made + session.lock(); + try { + rpcHandler.handleRpc(uI, request.getReader(), request); + + if (repaintAll) { + session.getCommunicationManager().repaintAll(uI); + } + + writeUidl(request, response, uI, outWriter, repaintAll, + analyzeLayouts); + postHandleRequest(uI); + } catch (JSONException e) { + getLogger().log(Level.SEVERE, "Error writing JSON to response", e); + // Refresh on client side + criticalNotifier.criticalNotification(request, response, null, + null, null, null); + } catch (InvalidUIDLSecurityKeyException e) { + getLogger().log(Level.WARNING, + "Invalid security key received from {}", + request.getRemoteHost()); + // Refresh on client side + criticalNotifier.criticalNotification(request, response, null, + null, null, null); + } finally { + session.unlock(); + outWriter.close(); + requestThemeName = null; + } + + // Ensure that the browser does not cache UIDL responses. + // iOS 6 Safari requires this (#9732) + response.setHeader("Cache-Control", "no-cache"); + + return true; + } + + /** + * Checks that the version reported by the client (widgetset) matches that + * of the server. + * + * @param request + */ + private void checkWidgetsetVersion(VaadinRequest request) { + String widgetsetVersion = request.getParameter("v-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)); + } + } + + private void writeUidl(VaadinRequest request, VaadinResponse response, + UI ui, Writer writer, boolean repaintAll, boolean analyzeLayouts) + throws IOException, JSONException { + openJsonMessage(writer, response); + + // security key + Object writeSecurityTokenFlag = request + .getAttribute(AbstractCommunicationManager.WRITE_SECURITY_TOKEN_FLAG); + + if (writeSecurityTokenFlag != null) { + writer.write(ui.getSession().getCommunicationManager() + .getSecurityKeyUIDL(request)); + } + + new UidlWriter().write(ui, writer, repaintAll, analyzeLayouts); + + closeJsonMessage(writer); + } + + /** + * Method called after the paint phase while still being synchronized on the + * session + * + * @param uI + * + */ + protected void postHandleRequest(UI uI) { + // Remove connectors that have been detached from the session during + // handling of the request + uI.getConnectorTracker().cleanConnectorMap(); + } + + protected void closeJsonMessage(Writer outWriter) throws IOException { + outWriter.write("}]"); + } + + /** + * Writes the opening of JSON message to be sent to client. + * + * @param outWriter + * @param response + * @throws IOException + */ + protected void openJsonMessage(Writer outWriter, VaadinResponse response) + throws IOException { + // Sets the response type + response.setContentType("application/json; charset=UTF-8"); + // some dirt to prevent cross site scripting + outWriter.write("for(;;);[{"); + } + + // TODO Does this belong here? + protected void highlightConnector(ClientConnector highlightedConnector) { + StringBuilder sb = new StringBuilder(); + sb.append("*** Debug details of a connector: *** \n"); + sb.append("Type: "); + sb.append(highlightedConnector.getClass().getName()); + sb.append("\nId:"); + sb.append(highlightedConnector.getConnectorId()); + if (highlightedConnector instanceof Component) { + Component component = (Component) highlightedConnector; + if (component.getCaption() != null) { + sb.append("\nCaption:"); + sb.append(component.getCaption()); + } + } + printHighlightedConnectorHierarchy(sb, highlightedConnector); + getLogger().info(sb.toString()); + } + + // TODO Does this belong here? + protected void printHighlightedConnectorHierarchy(StringBuilder sb, + ClientConnector connector) { + LinkedList h = new LinkedList(); + h.add(connector); + ClientConnector parent = connector.getParent(); + while (parent != null) { + h.addFirst(parent); + parent = parent.getParent(); + } + + sb.append("\nConnector hierarchy:\n"); + VaadinSession session2 = connector.getUI().getSession(); + sb.append(session2.getClass().getName()); + sb.append("("); + sb.append(session2.getClass().getSimpleName()); + sb.append(".java"); + sb.append(":1)"); + int l = 1; + for (ClientConnector connector2 : h) { + sb.append("\n"); + for (int i = 0; i < l; i++) { + sb.append(" "); + } + l++; + Class connectorClass = connector2 + .getClass(); + Class topClass = connectorClass; + while (topClass.getEnclosingClass() != null) { + topClass = topClass.getEnclosingClass(); + } + sb.append(connectorClass.getName()); + sb.append("("); + sb.append(topClass.getSimpleName()); + sb.append(".java:1)"); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(UidlRequestHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/UidlWriter.java b/server/src/com/vaadin/server/communication/UidlWriter.java new file mode 100644 index 0000000000..0086affe91 --- /dev/null +++ b/server/src/com/vaadin/server/communication/UidlWriter.java @@ -0,0 +1,315 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.annotations.JavaScript; +import com.vaadin.annotations.StyleSheet; +import com.vaadin.server.AbstractCommunicationManager; +import com.vaadin.server.AbstractCommunicationManager.ClientCache; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.JsonPaintTarget; +import com.vaadin.server.SystemMessages; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.UI; + +/** + * Serializes pending server-side changes to UI state to JSON. This includes + * shared state, client RPC invocations, connector hierarchy changes, connector + * type information among others. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class UidlWriter implements Serializable { + + /** + * Writes a JSON object containing all pending changes to the given UI. + * + * @param ui + * The {@link UI} whose changes to write + * @param writer + * The writer to use + * @param repaintAll + * Whether the client should re-render the whole UI. + * @param analyzeLayouts + * Whether detected layout problems should be logged. + * @throws IOException + * If the writing fails. + * @throws JSONException + * If the JSON serialization fails. + */ + public void write(UI ui, Writer writer, boolean repaintAll, + boolean analyzeLayouts) throws IOException, JSONException { + + ArrayList dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + VaadinSession session = ui.getSession(); + AbstractCommunicationManager manager = session + .getCommunicationManager(); + // Paints components + ConnectorTracker uiConnectorTracker = ui.getConnectorTracker(); + getLogger().log(Level.FINE, "* Creating response to client"); + + getLogger().log( + Level.FINE, + "Found " + dirtyVisibleConnectors.size() + + " dirty connectors to paint"); + for (ClientConnector connector : dirtyVisibleConnectors) { + boolean initialized = uiConnectorTracker + .isClientSideInitialized(connector); + connector.beforeClientResponse(!initialized); + } + + uiConnectorTracker.setWritingResponse(true); + try { + writer.write("\"changes\" : "); + + JsonPaintTarget paintTarget = new JsonPaintTarget(manager, writer, + !repaintAll); + + new LegacyUidlWriter().write(ui, writer, paintTarget); + + paintTarget.close(); + writer.write(", "); // 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. + + writer.write("\"state\":"); + new SharedStateWriter().write(ui, writer); + writer.write(", "); // 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 + + writer.write("\"types\":"); + new ConnectorTypeWriter().write(ui, writer, paintTarget); + writer.write(", "); // 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) + + writer.write("\"hierarchy\":"); + new ConnectorHierarchyWriter().write(ui, writer); + writer.write(", "); // close hierarchy + + uiConnectorTracker.markAllConnectorsClean(); + + // send server to client RPC calls for components in the UI, in call + // order + + // collect RPC calls from components in the UI in the order in + // which they were performed, remove the calls from components + + writer.write("\"rpc\" : "); + new ClientRpcWriter().write(ui, writer); + writer.write(", "); // close rpc + + writer.write("\"meta\" : "); + + SystemMessages messages = ui.getSession().getService() + .getSystemMessages(ui.getLocale(), null); + // TODO hilightedConnector + new MetadataWriter().write(ui, writer, repaintAll, analyzeLayouts, + null, messages); + writer.write(", "); + + writer.write("\"resources\" : "); + new ResourceWriter().write(ui, writer, paintTarget); + + Collection> usedClientConnectors = paintTarget + .getUsedClientConnectors(); + boolean typeMappingsOpen = false; + ClientCache clientCache = manager.getClientCache(ui); + + List> newConnectorTypes = new ArrayList>(); + + for (Class 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; + writer.write(", \"typeMappings\" : { "); + } else { + writer.write(" , "); + } + String canonicalName = class1.getCanonicalName(); + writer.write("\""); + writer.write(canonicalName); + writer.write("\" : "); + writer.write(manager.getTagForType(class1)); + } + } + if (typeMappingsOpen) { + writer.write(" }"); + } + + // TODO PUSH Refactor to TypeInheritanceWriter or something + boolean typeInheritanceMapOpen = false; + if (typeMappingsOpen) { + // send the whole type inheritance map if any new mappings + for (Class class1 : usedClientConnectors) { + if (!ClientConnector.class.isAssignableFrom(class1 + .getSuperclass())) { + continue; + } + if (!typeInheritanceMapOpen) { + typeInheritanceMapOpen = true; + writer.write(", \"typeInheritanceMap\" : { "); + } else { + writer.write(" , "); + } + writer.write("\""); + writer.write(manager.getTagForType(class1)); + writer.write("\" : "); + writer.write(manager + .getTagForType((Class) class1 + .getSuperclass())); + } + if (typeInheritanceMapOpen) { + writer.write(" }"); + } + } + + // TODO Refactor to DependencyWriter or something + /* + * 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>() { + @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 scriptDependencies = new ArrayList(); + List styleDependencies = new ArrayList(); + + for (Class class1 : newConnectorTypes) { + JavaScript jsAnnotation = class1 + .getAnnotation(JavaScript.class); + if (jsAnnotation != null) { + for (String uri : jsAnnotation.value()) { + scriptDependencies.add(manager.registerDependency(uri, + class1)); + } + } + + StyleSheet styleAnnotation = class1 + .getAnnotation(StyleSheet.class); + if (styleAnnotation != null) { + for (String uri : styleAnnotation.value()) { + styleDependencies.add(manager.registerDependency(uri, + class1)); + } + } + } + + // Include script dependencies in output if there are any + if (!scriptDependencies.isEmpty()) { + writer.write(", \"scriptDependencies\": " + + new JSONArray(scriptDependencies).toString()); + } + + // Include style dependencies in output if there are any + if (!styleDependencies.isEmpty()) { + writer.write(", \"styleDependencies\": " + + new JSONArray(styleDependencies).toString()); + } + + // add any pending locale definitions requested by the client + writer.write(", \"locales\": "); + manager.printLocaleDeclarations(writer); + + if (manager.getDragAndDropService() != null) { + manager.getDragAndDropService().printJSONResponse(writer); + } + + for (ClientConnector connector : dirtyVisibleConnectors) { + uiConnectorTracker.markClientSideInitialized(connector); + } + + assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended."; + + writePerformanceData(ui, writer); + } catch (IOException ex) { + throw new RuntimeException(ex); + } finally { + uiConnectorTracker.setWritingResponse(false); + } + } + + /** + * Adds the performance timing data (used by TestBench 3) to the UIDL + * response. + * + * @throws IOException + */ + private void writePerformanceData(UI ui, Writer writer) throws IOException { + writer.write(String.format(", \"timings\":[%d, %d]", ui.getSession() + .getCumulativeRequestDuration(), ui.getSession() + .getLastRequestDuration())); + } + + private static final Logger getLogger() { + return Logger.getLogger(UidlWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java index a229003224..7d741e1b28 100644 --- a/server/src/com/vaadin/ui/ConnectorTracker.java +++ b/server/src/com/vaadin/ui/ConnectorTracker.java @@ -17,6 +17,7 @@ package com.vaadin.ui; import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -463,6 +464,22 @@ public class ConnectorTracker implements Serializable { return dirtyConnectors; } + /** + * Returns a collection of those {@link #getDirtyConnectors() dirty + * connectors} that are actually visible to the client. + * + * @return A list of dirty and visible connectors. + */ + public ArrayList getDirtyVisibleConnectors() { + ArrayList dirtyConnectors = new ArrayList(); + for (ClientConnector c : getDirtyConnectors()) { + if (AbstractCommunicationManager.isConnectorVisibleToClient(c)) { + dirtyConnectors.add(c); + } + } + return dirtyConnectors; + } + public JSONObject getDiffState(ClientConnector connector) { assert getConnector(connector.getConnectorId()) == connector; return diffStates.get(connector); diff --git a/server/tests/src/com/vaadin/tests/server/TestSimpleMultiPartInputStream.java b/server/tests/src/com/vaadin/tests/server/TestSimpleMultiPartInputStream.java index 84247c81c1..6907594b5e 100644 --- a/server/tests/src/com/vaadin/tests/server/TestSimpleMultiPartInputStream.java +++ b/server/tests/src/com/vaadin/tests/server/TestSimpleMultiPartInputStream.java @@ -7,7 +7,7 @@ import java.util.Arrays; import junit.framework.TestCase; -import com.vaadin.server.AbstractCommunicationManager.SimpleMultiPartInputStream; +import com.vaadin.server.communication.FileUploadHandler.SimpleMultiPartInputStream; public class TestSimpleMultiPartInputStream extends TestCase { diff --git a/server/tests/src/com/vaadin/tests/server/TestStreamVariableMapping.java b/server/tests/src/com/vaadin/tests/server/TestStreamVariableMapping.java index 467a76dfa6..0245ec5d5e 100644 --- a/server/tests/src/com/vaadin/tests/server/TestStreamVariableMapping.java +++ b/server/tests/src/com/vaadin/tests/server/TestStreamVariableMapping.java @@ -67,7 +67,7 @@ public class TestStreamVariableMapping extends TestCase { assertNotNull(tracker.getStreamVariable(owner.getConnectorId(), variableName)); - cm.cleanStreamVariable(owner, variableName); + tracker.cleanStreamVariable(owner.getConnectorId(), variableName); assertNull(tracker.getStreamVariable(owner.getConnectorId(), variableName)); } -- 2.39.5