diff options
Diffstat (limited to 'src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java')
-rw-r--r-- | src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java | 2790 |
1 files changed, 0 insertions, 2790 deletions
diff --git a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java deleted file mode 100644 index ba1b3cadb6..0000000000 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ /dev/null @@ -1,2790 +0,0 @@ -/* -@VaadinApache2LicenseForJavaFiles@ - */ - -package com.vaadin.terminal.gwt.server; - -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.CharArrayWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.Serializable; -import java.io.StringWriter; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.text.CharacterIterator; -import java.text.DateFormat; -import java.text.DateFormatSymbols; -import java.text.SimpleDateFormat; -import java.text.StringCharacterIterator; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.servlet.http.HttpServletResponse; - -import com.vaadin.Application; -import com.vaadin.Application.SystemMessages; -import com.vaadin.RootRequiresMoreInformationException; -import com.vaadin.Version; -import com.vaadin.annotations.JavaScript; -import com.vaadin.annotations.StyleSheet; -import com.vaadin.external.json.JSONArray; -import com.vaadin.external.json.JSONException; -import com.vaadin.external.json.JSONObject; -import com.vaadin.shared.Connector; -import com.vaadin.shared.communication.MethodInvocation; -import com.vaadin.shared.communication.SharedState; -import com.vaadin.shared.communication.UidlValue; -import com.vaadin.terminal.AbstractClientConnector; -import com.vaadin.terminal.CombinedRequest; -import com.vaadin.terminal.LegacyPaint; -import com.vaadin.terminal.PaintException; -import com.vaadin.terminal.PaintTarget; -import com.vaadin.terminal.RequestHandler; -import com.vaadin.terminal.StreamVariable; -import com.vaadin.terminal.StreamVariable.StreamingEndEvent; -import com.vaadin.terminal.StreamVariable.StreamingErrorEvent; -import com.vaadin.terminal.Terminal.ErrorEvent; -import com.vaadin.terminal.Terminal.ErrorListener; -import com.vaadin.terminal.Vaadin6Component; -import com.vaadin.terminal.VariableOwner; -import com.vaadin.terminal.WrappedRequest; -import com.vaadin.terminal.WrappedResponse; -import com.vaadin.terminal.gwt.client.ApplicationConnection; -import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext; -import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; -import com.vaadin.terminal.gwt.server.RpcManager.RpcInvocationException; -import com.vaadin.ui.AbstractComponent; -import com.vaadin.ui.AbstractField; -import com.vaadin.ui.Component; -import com.vaadin.ui.ConnectorTracker; -import com.vaadin.ui.HasComponents; -import com.vaadin.ui.Root; -import com.vaadin.ui.Window; - -/** - * This is a common base class for the server-side implementations of the - * communication system between the client code (compiled with GWT into - * JavaScript) and the server side components. Its client side counterpart is - * {@link ApplicationConnection}. - * - * TODO Document better! - */ -@SuppressWarnings("serial") -public abstract class AbstractCommunicationManager implements Serializable { - - private static final String DASHDASH = "--"; - - private static final RequestHandler APP_RESOURCE_HANDLER = new ApplicationResourceHandler(); - - private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler(); - - /** - * TODO Document me! - * - * @author peholmst - */ - public interface Callback extends Serializable { - - public void criticalNotification(WrappedRequest request, - WrappedResponse response, String cap, String msg, - String details, String outOfSyncURL) throws IOException; - } - - static class UploadInterruptedException extends Exception { - public UploadInterruptedException() { - super("Upload interrupted by other thread"); - } - } - - private static String GET_PARAM_REPAINT_ALL = "repaintAll"; - - // flag used in the request to indicate that the security token should be - // written to the response - private static final String WRITE_SECURITY_TOKEN_FLAG = "writeSecurityToken"; - - /* Variable records indexes */ - public static final char VAR_BURST_SEPARATOR = '\u001d'; - - public static final char VAR_ESCAPE_CHARACTER = '\u001b'; - - private final HashMap<Integer, ClientCache> rootToClientCache = new HashMap<Integer, ClientCache>(); - - private static final int MAX_BUFFER_SIZE = 64 * 1024; - - /* Same as in apache commons file upload library that was previously used. */ - private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; - - private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; - - /** - * The application this communication manager is used for - */ - private final Application application; - - private List<String> locales; - - private int pendingLocalesIndex; - - private int timeoutInterval = -1; - - private DragAndDropService dragAndDropService; - - private String requestThemeName; - - private int maxInactiveInterval; - - private Connector highlightedConnector; - - private Map<String, Class<?>> connectorResourceContexts = new HashMap<String, Class<?>>(); - - private Map<String, Map<String, StreamVariable>> pidToNameToStreamVariable; - - private Map<StreamVariable, String> streamVariableToSeckey; - - /** - * TODO New constructor - document me! - * - * @param application - */ - public AbstractCommunicationManager(Application application) { - this.application = application; - application.addRequestHandler(getBootstrapHandler()); - application.addRequestHandler(APP_RESOURCE_HANDLER); - application.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER); - requireLocale(application.getLocale().toString()); - } - - protected Application getApplication() { - return application; - } - - private static final int LF = "\n".getBytes()[0]; - - private static final String CRLF = "\r\n"; - - private static final String UTF8 = "UTF8"; - - private static final String GET_PARAM_HIGHLIGHT_COMPONENT = "highlightComponent"; - - private static String readLine(InputStream stream) throws IOException { - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - int readByte = stream.read(); - while (readByte != LF) { - bout.write(readByte); - readByte = stream.read(); - } - byte[] bytes = bout.toByteArray(); - return new String(bytes, 0, bytes.length - 1, UTF8); - } - - /** - * Method used to stream content from a multipart request (either from - * servlet or portlet request) to given StreamVariable - * - * - * @param request - * @param response - * @param streamVariable - * @param owner - * @param boundary - * @throws IOException - */ - protected void doHandleSimpleMultipartFileUpload(WrappedRequest request, - WrappedResponse response, StreamVariable streamVariable, - String variableName, ClientConnector owner, String boundary) - throws IOException { - // multipart parsing, supports only one file for request, but that is - // fine for our current terminal - - final InputStream inputStream = request.getInputStream(); - - int contentLength = request.getContentLength(); - - boolean atStart = false; - boolean firstFileFieldFound = false; - - String rawfilename = "unknown"; - String rawMimeType = "application/octet-stream"; - - /* - * Read the stream until the actual file starts (empty line). Read - * filename and content type from multipart headers. - */ - while (!atStart) { - String readLine = readLine(inputStream); - contentLength -= (readLine.length() + 2); - if (readLine.startsWith("Content-Disposition:") - && readLine.indexOf("filename=") > 0) { - rawfilename = readLine.replaceAll(".*filename=", ""); - String parenthesis = rawfilename.substring(0, 1); - rawfilename = rawfilename.substring(1); - rawfilename = rawfilename.substring(0, - rawfilename.indexOf(parenthesis)); - firstFileFieldFound = true; - } else if (firstFileFieldFound && readLine.equals("")) { - atStart = true; - } else if (readLine.startsWith("Content-Type")) { - rawMimeType = readLine.split(": ")[1]; - } - } - - contentLength -= (boundary.length() + CRLF.length() + 2 - * DASHDASH.length() + 2); // 2 == CRLF - - /* - * Reads bytes from the underlying stream. Compares the read bytes to - * the boundary string and returns -1 if met. - * - * The matching happens so that if the read byte equals to the first - * char of boundary string, the stream goes to "buffering mode". In - * buffering mode bytes are read until the character does not match the - * corresponding from boundary string or the full boundary string is - * found. - * - * Note, if this is someday needed elsewhere, don't shoot yourself to - * foot and split to a top level helper class. - */ - InputStream simpleMultiPartReader = new SimpleMultiPartInputStream( - inputStream, boundary); - - /* - * Should report only the filename even if the browser sends the path - */ - final String filename = removePath(rawfilename); - final String mimeType = rawMimeType; - - try { - // TODO Shouldn't this check connectorEnabled? - if (owner == null) { - throw new UploadException( - "File upload ignored because the connector for the stream variable was not found"); - } - if (owner instanceof Component) { - if (((Component) owner).isReadOnly()) { - throw new UploadException( - "Warning: file upload ignored because the componente was read-only"); - } - } - boolean forgetVariable = streamToReceiver(simpleMultiPartReader, - streamVariable, filename, mimeType, contentLength); - if (forgetVariable) { - cleanStreamVariable(owner, variableName); - } - } catch (Exception e) { - synchronized (application) { - handleChangeVariablesError(application, (Component) owner, e, - new HashMap<String, Object>()); - } - } - sendUploadResponse(request, response); - - } - - /** - * Used to stream plain file post (aka XHR2.post(File)) - * - * @param request - * @param response - * @param streamVariable - * @param owner - * @param contentLength - * @throws IOException - */ - protected void doHandleXhrFilePost(WrappedRequest request, - WrappedResponse response, StreamVariable streamVariable, - String variableName, ClientConnector owner, int contentLength) - throws IOException { - - // These are unknown in filexhr ATM, maybe add to Accept header that - // is accessible in portlets - final String filename = "unknown"; - final String mimeType = filename; - final InputStream stream = request.getInputStream(); - try { - /* - * safe cast as in GWT terminal all variable owners are expected to - * be components. - */ - Component component = (Component) owner; - if (component.isReadOnly()) { - throw new UploadException( - "Warning: file upload ignored because the component was read-only"); - } - boolean forgetVariable = streamToReceiver(stream, streamVariable, - filename, mimeType, contentLength); - if (forgetVariable) { - cleanStreamVariable(owner, variableName); - } - } catch (Exception e) { - synchronized (application) { - handleChangeVariablesError(application, (Component) owner, e, - new HashMap<String, Object>()); - } - } - sendUploadResponse(request, response); - } - - /** - * @param in - * @param streamVariable - * @param filename - * @param type - * @param contentLength - * @return true if the streamvariable has informed that the terminal can - * forget this variable - * @throws UploadException - */ - protected final boolean streamToReceiver(final InputStream in, - StreamVariable streamVariable, String filename, String type, - int contentLength) throws UploadException { - if (streamVariable == null) { - throw new IllegalStateException( - "StreamVariable for the post not found"); - } - - final Application application = getApplication(); - - OutputStream out = null; - int totalBytes = 0; - StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( - filename, type, contentLength); - try { - boolean listenProgress; - synchronized (application) { - streamVariable.streamingStarted(startedEvent); - out = streamVariable.getOutputStream(); - listenProgress = streamVariable.listenProgress(); - } - - // Gets the output target stream - if (out == null) { - throw new NoOutputStreamException(); - } - - if (null == in) { - // No file, for instance non-existent filename in html upload - throw new NoInputStreamException(); - } - - final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE]; - int bytesReadToBuffer = 0; - while ((bytesReadToBuffer = in.read(buffer)) > 0) { - out.write(buffer, 0, bytesReadToBuffer); - totalBytes += bytesReadToBuffer; - if (listenProgress) { - // update progress if listener set and contentLength - // received - synchronized (application) { - StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( - filename, type, contentLength, totalBytes); - streamVariable.onProgress(progressEvent); - } - } - if (streamVariable.isInterrupted()) { - throw new UploadInterruptedException(); - } - } - - // upload successful - out.close(); - StreamingEndEvent event = new StreamingEndEventImpl(filename, type, - totalBytes); - synchronized (application) { - streamVariable.streamingFinished(event); - } - - } catch (UploadInterruptedException e) { - // Download interrupted by application code - tryToCloseStream(out); - StreamingErrorEvent event = new StreamingErrorEventImpl(filename, - type, contentLength, totalBytes, e); - synchronized (application) { - streamVariable.streamingFailed(event); - } - // Note, we are not throwing interrupted exception forward as it is - // not a terminal level error like all other exception. - } catch (final Exception e) { - tryToCloseStream(out); - synchronized (application) { - StreamingErrorEvent event = new StreamingErrorEventImpl( - filename, type, contentLength, totalBytes, e); - synchronized (application) { - streamVariable.streamingFailed(event); - } - // throw exception for terminal to be handled (to be passed to - // terminalErrorHandler) - throw new UploadException(e); - } - } - return startedEvent.isDisposed(); - } - - static void tryToCloseStream(OutputStream out) { - try { - // try to close output stream (e.g. file handle) - if (out != null) { - out.close(); - } - } catch (IOException e1) { - // NOP - } - } - - /** - * Removes any possible path information from the filename and returns the - * filename. Separators / and \\ are used. - * - * @param name - * @return - */ - private static String removePath(String filename) { - if (filename != null) { - filename = filename.replaceAll("^.*[/\\\\]", ""); - } - - return filename; - } - - /** - * TODO document - * - * @param request - * @param response - * @throws IOException - */ - protected void sendUploadResponse(WrappedRequest request, - WrappedResponse response) throws IOException { - response.setContentType("text/html"); - final OutputStream out = response.getOutputStream(); - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - outWriter.print("<html><body>download handled</body></html>"); - outWriter.flush(); - out.close(); - } - - /** - * Internally process a UIDL request from the client. - * - * This method calls - * {@link #handleVariables(WrappedRequest, WrappedResponse, Callback, Application, Root)} - * to process any changes to variables by the client and then repaints - * affected components using {@link #paintAfterVariableChanges()}. - * - * Also, some cleanup is done when a request arrives for an application that - * has already been closed. - * - * The method handleUidlRequest(...) in subclasses should call this method. - * - * TODO better documentation - * - * @param request - * @param response - * @param callback - * @param root - * target window for the UIDL request, can be null if target not - * found - * @throws IOException - * @throws InvalidUIDLSecurityKeyException - * @throws JSONException - */ - public void handleUidlRequest(WrappedRequest request, - WrappedResponse response, Callback callback, Root root) - throws IOException, InvalidUIDLSecurityKeyException, JSONException { - - checkWidgetsetVersion(request); - requestThemeName = request.getParameter("theme"); - maxInactiveInterval = request.getSessionMaxInactiveInterval(); - // repaint requested or session has timed out and new one is created - boolean repaintAll; - final OutputStream out; - - repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null); - // || (request.getSession().isNew()); FIXME What the h*ll is this?? - out = response.getOutputStream(); - - boolean analyzeLayouts = false; - if (repaintAll) { - // analyzing can be done only with repaintAll - analyzeLayouts = (request.getParameter(GET_PARAM_ANALYZE_LAYOUTS) != null); - - if (request.getParameter(GET_PARAM_HIGHLIGHT_COMPONENT) != null) { - String pid = request - .getParameter(GET_PARAM_HIGHLIGHT_COMPONENT); - highlightedConnector = root.getConnectorTracker().getConnector( - pid); - highlightConnector(highlightedConnector); - } - } - - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - - // The rest of the process is synchronized with the application - // in order to guarantee that no parallel variable handling is - // made - synchronized (application) { - - // Finds the window within the application - if (application.isRunning()) { - // Returns if no window found - if (root == null) { - // This should not happen, no windows exists but - // application is still open. - getLogger().warning("Could not get root for application"); - return; - } - } else { - // application has been closed - endApplication(request, response, application); - return; - } - - // Change all variables based on request parameters - if (!handleVariables(request, response, callback, application, root)) { - - // var inconsistency; the client is probably out-of-sync - SystemMessages ci = null; - try { - Method m = application.getClass().getMethod( - "getSystemMessages", (Class[]) null); - ci = (Application.SystemMessages) m.invoke(null, - (Object[]) null); - } catch (Exception e2) { - // FIXME: Handle exception - // Not critical, but something is still wrong; print - // stacktrace - getLogger().log(Level.WARNING, - "getSystemMessages() failed - continuing", e2); - } - if (ci != null) { - String msg = ci.getOutOfSyncMessage(); - String cap = ci.getOutOfSyncCaption(); - if (msg != null || cap != null) { - callback.criticalNotification(request, response, cap, - msg, null, ci.getOutOfSyncURL()); - // will reload page after this - return; - } - } - // No message to show, let's just repaint all. - repaintAll = true; - } - - paintAfterVariableChanges(request, response, callback, repaintAll, - outWriter, root, analyzeLayouts); - postPaint(root); - } - - outWriter.close(); - requestThemeName = null; - } - - /** - * Checks that the version reported by the client (widgetset) matches that - * of the server. - * - * @param request - */ - private void checkWidgetsetVersion(WrappedRequest request) { - String widgetsetVersion = request.getParameter("wsver"); - if (widgetsetVersion == null) { - // Only check when the widgetset version is reported. It is reported - // in the first UIDL request (not the initial request as it is a - // plain GET /) - return; - } - - if (!Version.getFullVersion().equals(widgetsetVersion)) { - getLogger().warning( - String.format(Constants.WIDGETSET_MISMATCH_INFO, - Version.getFullVersion(), widgetsetVersion)); - } - } - - /** - * Method called after the paint phase while still being synchronized on the - * application - * - * @param root - * - */ - protected void postPaint(Root root) { - // Remove connectors that have been detached from the application during - // handling of the request - root.getConnectorTracker().cleanConnectorMap(); - - if (pidToNameToStreamVariable != null) { - Iterator<String> iterator = pidToNameToStreamVariable.keySet() - .iterator(); - while (iterator.hasNext()) { - String connectorId = iterator.next(); - if (root.getConnectorTracker().getConnector(connectorId) == null) { - // Owner is no longer attached to the application - Map<String, StreamVariable> removed = pidToNameToStreamVariable - .get(connectorId); - for (String key : removed.keySet()) { - streamVariableToSeckey.remove(removed.get(key)); - } - iterator.remove(); - } - } - } - } - - protected void highlightConnector(Connector highlightedConnector) { - StringBuilder sb = new StringBuilder(); - sb.append("*** Debug details of a component: *** \n"); - sb.append("Type: "); - sb.append(highlightedConnector.getClass().getName()); - if (highlightedConnector instanceof AbstractComponent) { - AbstractComponent component = (AbstractComponent) highlightedConnector; - sb.append("\nId:"); - sb.append(highlightedConnector.getConnectorId()); - if (component.getCaption() != null) { - sb.append("\nCaption:"); - sb.append(component.getCaption()); - } - - printHighlightedComponentHierarchy(sb, component); - } - getLogger().info(sb.toString()); - } - - protected void printHighlightedComponentHierarchy(StringBuilder sb, - AbstractComponent component) { - LinkedList<Component> h = new LinkedList<Component>(); - h.add(component); - Component parent = component.getParent(); - while (parent != null) { - h.addFirst(parent); - parent = parent.getParent(); - } - - sb.append("\nComponent hierarchy:\n"); - Application application2 = component.getApplication(); - sb.append(application2.getClass().getName()); - sb.append("."); - sb.append(application2.getClass().getSimpleName()); - sb.append("("); - sb.append(application2.getClass().getSimpleName()); - sb.append(".java"); - sb.append(":1)"); - int l = 1; - for (Component component2 : h) { - sb.append("\n"); - for (int i = 0; i < l; i++) { - sb.append(" "); - } - l++; - Class<? extends Component> componentClass = component2.getClass(); - Class<?> topClass = componentClass; - while (topClass.getEnclosingClass() != null) { - topClass = topClass.getEnclosingClass(); - } - sb.append(componentClass.getName()); - sb.append("."); - sb.append(componentClass.getSimpleName()); - sb.append("("); - sb.append(topClass.getSimpleName()); - sb.append(".java:1)"); - } - } - - /** - * TODO document - * - * @param request - * @param response - * @param callback - * @param repaintAll - * @param outWriter - * @param window - * @param analyzeLayouts - * @throws PaintException - * @throws IOException - * @throws JSONException - */ - private void paintAfterVariableChanges(WrappedRequest request, - WrappedResponse response, Callback callback, boolean repaintAll, - final PrintWriter outWriter, Root root, boolean analyzeLayouts) - throws PaintException, IOException, JSONException { - - // Removes application if it has stopped during variable changes - if (!application.isRunning()) { - endApplication(request, response, application); - return; - } - - openJsonMessage(outWriter, response); - - // security key - Object writeSecurityTokenFlag = request - .getAttribute(WRITE_SECURITY_TOKEN_FLAG); - - if (writeSecurityTokenFlag != null) { - outWriter.print(getSecurityKeyUIDL(request)); - } - - writeUidlResponse(request, repaintAll, outWriter, root, analyzeLayouts); - - closeJsonMessage(outWriter); - - outWriter.close(); - - } - - /** - * Gets the security key (and generates one if needed) as UIDL. - * - * @param request - * @return the security key UIDL or "" if the feature is turned off - */ - public String getSecurityKeyUIDL(WrappedRequest request) { - final String seckey = getSecurityKey(request); - if (seckey != null) { - return "\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID - + "\":\"" + seckey + "\","; - } else { - return ""; - } - } - - /** - * Gets the security key (and generates one if needed). - * - * @param request - * @return the security key - */ - protected String getSecurityKey(WrappedRequest request) { - String seckey = null; - seckey = (String) request - .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); - if (seckey == null) { - seckey = UUID.randomUUID().toString(); - request.setSessionAttribute( - ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey); - } - - return seckey; - } - - @SuppressWarnings("unchecked") - public void writeUidlResponse(WrappedRequest request, boolean repaintAll, - final PrintWriter outWriter, Root root, boolean analyzeLayouts) - throws PaintException, JSONException { - ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>(); - Application application = root.getApplication(); - // Paints components - ConnectorTracker rootConnectorTracker = root.getConnectorTracker(); - getLogger().log(Level.FINE, "* Creating response to client"); - if (repaintAll) { - getClientCache(root).clear(); - rootConnectorTracker.markAllConnectorsDirty(); - - // Reset sent locales - locales = null; - requireLocale(application.getLocale().toString()); - } - - dirtyVisibleConnectors - .addAll(getDirtyVisibleConnectors(rootConnectorTracker)); - - getLogger().log( - Level.FINE, - "Found " + dirtyVisibleConnectors.size() - + " dirty connectors to paint"); - for (ClientConnector connector : dirtyVisibleConnectors) { - if (connector instanceof Component) { - ((Component) connector).updateState(); - } - } - rootConnectorTracker.markAllConnectorsClean(); - - outWriter.print("\"changes\":["); - - List<InvalidLayout> invalidComponentRelativeSizes = null; - - JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, - !repaintAll); - legacyPaint(paintTarget, dirtyVisibleConnectors); - - if (analyzeLayouts) { - invalidComponentRelativeSizes = ComponentSizeValidator - .validateComponentRelativeSizes(root.getContent(), null, - null); - - // Also check any existing subwindows - if (root.getWindows() != null) { - for (Window subWindow : root.getWindows()) { - invalidComponentRelativeSizes = ComponentSizeValidator - .validateComponentRelativeSizes( - subWindow.getContent(), - invalidComponentRelativeSizes, null); - } - } - } - - paintTarget.close(); - outWriter.print("], "); // close changes - - // send shared state to client - - // for now, send the complete state of all modified and new - // components - - // Ideally, all this would be sent before "changes", but that causes - // complications with legacy components that create sub-components - // in their paint phase. Nevertheless, this will be processed on the - // client after component creation but before legacy UIDL - // processing. - JSONObject sharedStates = new JSONObject(); - for (ClientConnector connector : dirtyVisibleConnectors) { - SharedState state = connector.getState(); - if (null != state) { - // encode and send shared state - try { - Class<? extends SharedState> stateType = connector - .getStateType(); - SharedState referenceState = null; - if (repaintAll) { - // Use an empty state object as reference for full - // repaints - try { - referenceState = stateType.newInstance(); - } catch (Exception e) { - getLogger().log( - Level.WARNING, - "Error creating reference object for state of type " - + stateType.getName()); - } - } - Object stateJson = JsonCodec.encode(state, referenceState, - stateType, root.getConnectorTracker()); - - sharedStates.put(connector.getConnectorId(), stateJson); - } catch (JSONException e) { - throw new PaintException( - "Failed to serialize shared state for connector " - + connector.getClass().getName() + " (" - + connector.getConnectorId() + "): " - + e.getMessage(), e); - } - } - } - outWriter.print("\"state\":"); - outWriter.append(sharedStates.toString()); - outWriter.print(", "); // close states - - // TODO This should be optimized. The type only needs to be - // sent once for each connector id + on refresh. Use the same cache as - // widget mapping - - JSONObject connectorTypes = new JSONObject(); - for (ClientConnector connector : dirtyVisibleConnectors) { - String connectorType = paintTarget.getTag(connector); - try { - connectorTypes.put(connector.getConnectorId(), connectorType); - } catch (JSONException e) { - throw new PaintException( - "Failed to send connector type for connector " - + connector.getConnectorId() + ": " - + e.getMessage(), e); - } - } - outWriter.print("\"types\":"); - outWriter.append(connectorTypes.toString()); - outWriter.print(", "); // close states - - // Send update hierarchy information to the client. - - // This could be optimized aswell to send only info if hierarchy has - // actually changed. Much like with the shared state. Note though - // that an empty hierarchy is information aswell (e.g. change from 1 - // child to 0 children) - - outWriter.print("\"hierarchy\":"); - - JSONObject hierarchyInfo = new JSONObject(); - for (ClientConnector connector : dirtyVisibleConnectors) { - String connectorId = connector.getConnectorId(); - JSONArray children = new JSONArray(); - - for (ClientConnector child : AbstractClientConnector - .getAllChildrenIterable(connector)) { - if (isVisible(child)) { - children.put(child.getConnectorId()); - } - } - try { - hierarchyInfo.put(connectorId, children); - } catch (JSONException e) { - throw new PaintException( - "Failed to send hierarchy information about " - + connectorId + " to the client: " - + e.getMessage(), e); - } - } - outWriter.append(hierarchyInfo.toString()); - outWriter.print(", "); // close hierarchy - - // send server to client RPC calls for components in the root, in call - // order - - // collect RPC calls from components in the root in the order in - // which they were performed, remove the calls from components - - LinkedList<ClientConnector> rpcPendingQueue = new LinkedList<ClientConnector>( - dirtyVisibleConnectors); - List<ClientMethodInvocation> pendingInvocations = collectPendingRpcCalls(dirtyVisibleConnectors); - - JSONArray rpcCalls = new JSONArray(); - for (ClientMethodInvocation invocation : pendingInvocations) { - // add invocation to rpcCalls - try { - JSONArray invocationJson = new JSONArray(); - invocationJson.put(invocation.getConnector().getConnectorId()); - invocationJson.put(invocation.getInterfaceName()); - invocationJson.put(invocation.getMethodName()); - JSONArray paramJson = new JSONArray(); - for (int i = 0; i < invocation.getParameterTypes().length; ++i) { - Type parameterType = invocation.getParameterTypes()[i]; - Object referenceParameter = null; - // TODO Use default values for RPC parameter types - // if (!JsonCodec.isInternalType(parameterType)) { - // try { - // referenceParameter = parameterType.newInstance(); - // } catch (Exception e) { - // logger.log(Level.WARNING, - // "Error creating reference object for parameter of type " - // + parameterType.getName()); - // } - // } - paramJson.put(JsonCodec.encode( - invocation.getParameters()[i], referenceParameter, - parameterType, root.getConnectorTracker())); - } - invocationJson.put(paramJson); - rpcCalls.put(invocationJson); - } catch (JSONException e) { - throw new PaintException( - "Failed to serialize RPC method call parameters for connector " - + invocation.getConnector().getConnectorId() - + " method " + invocation.getInterfaceName() - + "." + invocation.getMethodName() + ": " - + e.getMessage(), e); - } - - } - - if (rpcCalls.length() > 0) { - outWriter.print("\"rpc\" : "); - outWriter.append(rpcCalls.toString()); - outWriter.print(", "); // close rpc - } - - outWriter.print("\"meta\" : {"); - boolean metaOpen = false; - - if (repaintAll) { - metaOpen = true; - outWriter.write("\"repaintAll\":true"); - if (analyzeLayouts) { - outWriter.write(", \"invalidLayouts\":"); - outWriter.write("["); - if (invalidComponentRelativeSizes != null) { - boolean first = true; - for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { - if (!first) { - outWriter.write(","); - } else { - first = false; - } - invalidLayout.reportErrors(outWriter, this, System.err); - } - } - outWriter.write("]"); - } - if (highlightedConnector != null) { - outWriter.write(", \"hl\":\""); - outWriter.write(highlightedConnector.getConnectorId()); - outWriter.write("\""); - highlightedConnector = null; - } - } - - SystemMessages ci = null; - try { - Method m = application.getClass().getMethod("getSystemMessages", - (Class[]) null); - ci = (Application.SystemMessages) m.invoke(null, (Object[]) null); - } catch (NoSuchMethodException e) { - getLogger().log(Level.WARNING, - "getSystemMessages() failed - continuing", e); - } catch (IllegalArgumentException e) { - getLogger().log(Level.WARNING, - "getSystemMessages() failed - continuing", e); - } catch (IllegalAccessException e) { - getLogger().log(Level.WARNING, - "getSystemMessages() failed - continuing", e); - } catch (InvocationTargetException e) { - getLogger().log(Level.WARNING, - "getSystemMessages() failed - continuing", e); - } - - // meta instruction for client to enable auto-forward to - // sessionExpiredURL after timer expires. - if (ci != null && ci.getSessionExpiredMessage() == null - && ci.getSessionExpiredCaption() == null - && ci.isSessionExpiredNotificationEnabled()) { - int newTimeoutInterval = getTimeoutInterval(); - if (repaintAll || (timeoutInterval != newTimeoutInterval)) { - String escapedURL = ci.getSessionExpiredURL() == null ? "" : ci - .getSessionExpiredURL().replace("/", "\\/"); - if (metaOpen) { - outWriter.write(","); - } - outWriter.write("\"timedRedirect\":{\"interval\":" - + (newTimeoutInterval + 15) + ",\"url\":\"" - + escapedURL + "\"}"); - metaOpen = true; - } - timeoutInterval = newTimeoutInterval; - } - - outWriter.print("}, \"resources\" : {"); - - // Precache custom layouts - - // TODO We should only precache the layouts that are not - // cached already (plagiate from usedPaintableTypes) - int resourceIndex = 0; - for (final Iterator<Object> i = paintTarget.getUsedResources() - .iterator(); i.hasNext();) { - final String resource = (String) i.next(); - InputStream is = null; - try { - is = getThemeResourceAsStream(root, getTheme(root), resource); - } catch (final Exception e) { - // FIXME: Handle exception - getLogger().log(Level.FINER, - "Failed to get theme resource stream.", e); - } - if (is != null) { - - outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\"" - + resource + "\" : "); - final StringBuffer layout = new StringBuffer(); - - try { - final InputStreamReader r = new InputStreamReader(is, - "UTF-8"); - final char[] buffer = new char[20000]; - int charsRead = 0; - while ((charsRead = r.read(buffer)) > 0) { - layout.append(buffer, 0, charsRead); - } - r.close(); - } catch (final java.io.IOException e) { - // FIXME: Handle exception - getLogger().log(Level.INFO, "Resource transfer failed", e); - } - outWriter.print("\"" - + JsonPaintTarget.escapeJSON(layout.toString()) + "\""); - } else { - // FIXME: Handle exception - getLogger().severe("CustomLayout not found: " + resource); - } - } - outWriter.print("}"); - - Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget - .getUsedClientConnectors(); - boolean typeMappingsOpen = false; - ClientCache clientCache = getClientCache(root); - - List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>(); - - for (Class<? extends ClientConnector> class1 : usedClientConnectors) { - if (clientCache.cache(class1)) { - // client does not know the mapping key for this type, send - // mapping to client - newConnectorTypes.add(class1); - - if (!typeMappingsOpen) { - typeMappingsOpen = true; - outWriter.print(", \"typeMappings\" : { "); - } else { - outWriter.print(" , "); - } - String canonicalName = class1.getCanonicalName(); - outWriter.print("\""); - outWriter.print(canonicalName); - outWriter.print("\" : "); - outWriter.print(getTagForType(class1)); - } - } - if (typeMappingsOpen) { - outWriter.print(" }"); - } - - boolean typeInheritanceMapOpen = false; - if (typeMappingsOpen) { - // send the whole type inheritance map if any new mappings - for (Class<? extends ClientConnector> class1 : usedClientConnectors) { - if (!ClientConnector.class.isAssignableFrom(class1 - .getSuperclass())) { - continue; - } - if (!typeInheritanceMapOpen) { - typeInheritanceMapOpen = true; - outWriter.print(", \"typeInheritanceMap\" : { "); - } else { - outWriter.print(" , "); - } - outWriter.print("\""); - outWriter.print(getTagForType(class1)); - outWriter.print("\" : "); - outWriter - .print(getTagForType((Class<? extends ClientConnector>) class1 - .getSuperclass())); - } - if (typeInheritanceMapOpen) { - outWriter.print(" }"); - } - } - - /* - * Ensure super classes come before sub classes to get script dependency - * order right. Sub class @JavaScript might assume that @JavaScript - * defined by super class is already loaded. - */ - Collections.sort(newConnectorTypes, new Comparator<Class<?>>() { - @Override - public int compare(Class<?> o1, Class<?> o2) { - // TODO optimize using Class.isAssignableFrom? - return hierarchyDepth(o1) - hierarchyDepth(o2); - } - - private int hierarchyDepth(Class<?> type) { - if (type == Object.class) { - return 0; - } else { - return hierarchyDepth(type.getSuperclass()) + 1; - } - } - }); - - List<String> scriptDependencies = new ArrayList<String>(); - List<String> styleDependencies = new ArrayList<String>(); - - for (Class<? extends ClientConnector> class1 : newConnectorTypes) { - JavaScript jsAnnotation = class1.getAnnotation(JavaScript.class); - if (jsAnnotation != null) { - for (String resource : jsAnnotation.value()) { - scriptDependencies.add(registerResource(resource, class1)); - } - } - - StyleSheet styleAnnotation = class1.getAnnotation(StyleSheet.class); - if (styleAnnotation != null) { - for (String resource : styleAnnotation.value()) { - styleDependencies.add(registerResource(resource, class1)); - } - } - } - - // Include script dependencies in output if there are any - if (!scriptDependencies.isEmpty()) { - outWriter.print(", \"scriptDependencies\": " - + new JSONArray(scriptDependencies).toString()); - } - - // Include style dependencies in output if there are any - if (!styleDependencies.isEmpty()) { - outWriter.print(", \"styleDependencies\": " - + new JSONArray(styleDependencies).toString()); - } - - // add any pending locale definitions requested by the client - printLocaleDeclarations(outWriter); - - if (dragAndDropService != null) { - dragAndDropService.printJSONResponse(outWriter); - } - - writePerformanceData(outWriter); - } - - /** - * Resolves a resource URI, registering the URI with this - * {@code AbstractCommunicationManager} if needed and returns a fully - * qualified URI. - */ - private String registerResource(String resourceUri, Class<?> context) { - try { - URI uri = new URI(resourceUri); - String protocol = uri.getScheme(); - - if ("connector".equals(protocol)) { - // Strip initial slash - String resourceName = uri.getPath().substring(1); - return registerConnectorResource(resourceName, context); - } - - if (protocol != null || uri.getHost() != null) { - return resourceUri; - } - - // Bare path interpreted as connector resource - return registerConnectorResource(resourceUri, context); - } catch (URISyntaxException e) { - getLogger().log(Level.WARNING, - "Could not parse resource url " + resourceUri, e); - return resourceUri; - } - } - - private String registerConnectorResource(String name, Class<?> context) { - synchronized (connectorResourceContexts) { - // Add to map of names accepted by serveConnectorResource - if (connectorResourceContexts.containsKey(name)) { - Class<?> oldContext = connectorResourceContexts.get(name); - if (oldContext != context) { - getLogger().warning( - "Resource " + name + " defined by both " + context - + " and " + oldContext + ". Resource from " - + oldContext + " will be used."); - } - } else { - connectorResourceContexts.put(name, context); - } - } - - return ApplicationConnection.CONNECTOR_PROTOCOL_PREFIX + "/" + name; - } - - /** - * Adds the performance timing data (used by TestBench 3) to the UIDL - * response. - */ - private void writePerformanceData(final PrintWriter outWriter) { - AbstractWebApplicationContext ctx = (AbstractWebApplicationContext) application - .getContext(); - outWriter.write(String.format(", \"timings\":[%d, %d]", - ctx.getTotalSessionTime(), ctx.getLastRequestTime())); - } - - private void legacyPaint(PaintTarget paintTarget, - ArrayList<ClientConnector> dirtyVisibleConnectors) - throws PaintException { - List<Vaadin6Component> legacyComponents = new ArrayList<Vaadin6Component>(); - for (Connector connector : dirtyVisibleConnectors) { - // All Components that want to use paintContent must implement - // Vaadin6Component - if (connector instanceof Vaadin6Component) { - legacyComponents.add((Vaadin6Component) connector); - } - } - sortByHierarchy((List) legacyComponents); - for (Vaadin6Component c : legacyComponents) { - getLogger().fine( - "Painting Vaadin6Component " + c.getClass().getName() + "@" - + Integer.toHexString(c.hashCode())); - paintTarget.startTag("change"); - final String pid = c.getConnectorId(); - paintTarget.addAttribute("pid", pid); - LegacyPaint.paint(c, paintTarget); - paintTarget.endTag("change"); - } - - } - - private void sortByHierarchy(List<Component> paintables) { - // Vaadin 6 requires parents to be painted before children as component - // containers rely on that their updateFromUIDL method has been called - // before children start calling e.g. updateCaption - Collections.sort(paintables, new Comparator<Component>() { - - @Override - public int compare(Component c1, Component c2) { - int depth1 = 0; - while (c1.getParent() != null) { - depth1++; - c1 = c1.getParent(); - } - int depth2 = 0; - while (c2.getParent() != null) { - depth2++; - c2 = c2.getParent(); - } - if (depth1 < depth2) { - return -1; - } - if (depth1 > depth2) { - return 1; - } - return 0; - } - }); - - } - - private ClientCache getClientCache(Root root) { - Integer rootId = Integer.valueOf(root.getRootId()); - ClientCache cache = rootToClientCache.get(rootId); - if (cache == null) { - cache = new ClientCache(); - rootToClientCache.put(rootId, cache); - } - return cache; - } - - /** - * Checks if the connector is visible in context. For Components, - * {@link #isVisible(Component)} is used. For other types of connectors, the - * contextual visibility of its first Component ancestor is used. If no - * Component ancestor is found, the connector is not visible. - * - * @param connector - * The connector to check - * @return <code>true</code> if the connector is visible to the client, - * <code>false</code> otherwise - */ - static boolean isVisible(ClientConnector connector) { - if (connector instanceof Component) { - return isVisible((Component) connector); - } else { - ClientConnector parent = connector.getParent(); - if (parent == null) { - return false; - } else { - return isVisible(parent); - } - } - } - - /** - * Checks if the component is visible in context, i.e. returns false if the - * child is hidden, the parent is hidden or the parent says the child should - * not be rendered (using - * {@link HasComponents#isComponentVisible(Component)} - * - * @param child - * The child to check - * @return true if the child is visible to the client, false otherwise - */ - static boolean isVisible(Component child) { - if (!child.isVisible()) { - return false; - } - - HasComponents parent = child.getParent(); - if (parent == null) { - if (child instanceof Root) { - return child.isVisible(); - } else { - return false; - } - } - - return parent.isComponentVisible(child) && isVisible(parent); - } - - private static class NullIterator<E> implements Iterator<E> { - - @Override - public boolean hasNext() { - return false; - } - - @Override - public E next() { - return null; - } - - @Override - public void remove() { - } - - } - - /** - * Collects all pending RPC calls from listed {@link ClientConnector}s and - * clears their RPC queues. - * - * @param rpcPendingQueue - * list of {@link ClientConnector} of interest - * @return ordered list of pending RPC calls - */ - private List<ClientMethodInvocation> collectPendingRpcCalls( - List<ClientConnector> rpcPendingQueue) { - List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); - for (ClientConnector connector : rpcPendingQueue) { - List<ClientMethodInvocation> paintablePendingRpc = connector - .retrievePendingRpcCalls(); - if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { - List<ClientMethodInvocation> oldPendingRpc = pendingInvocations; - int totalCalls = pendingInvocations.size() - + paintablePendingRpc.size(); - pendingInvocations = new ArrayList<ClientMethodInvocation>( - totalCalls); - - // merge two ordered comparable lists - for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { - if (paintableIndex >= paintablePendingRpc.size() - || (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc - .get(oldIndex)) - .compareTo(paintablePendingRpc - .get(paintableIndex)) <= 0)) { - pendingInvocations.add(oldPendingRpc.get(oldIndex++)); - } else { - pendingInvocations.add(paintablePendingRpc - .get(paintableIndex++)); - } - } - } - } - return pendingInvocations; - } - - protected abstract InputStream getThemeResourceAsStream(Root root, - String themeName, String resource); - - private int getTimeoutInterval() { - return maxInactiveInterval; - } - - private String getTheme(Root root) { - String themeName = root.getApplication().getThemeForRoot(root); - String requestThemeName = getRequestTheme(); - - if (requestThemeName != null) { - themeName = requestThemeName; - } - if (themeName == null) { - themeName = AbstractApplicationServlet.getDefaultTheme(); - } - return themeName; - } - - private String getRequestTheme() { - return requestThemeName; - } - - /** - * Returns false if the cross site request forgery protection is turned off. - * - * @param application - * @return false if the XSRF is turned off, true otherwise - */ - public boolean isXSRFEnabled(Application application) { - return !"true" - .equals(application - .getProperty(AbstractApplicationServlet.SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION)); - } - - /** - * TODO document - * - * If this method returns false, something was submitted that we did not - * expect; this is probably due to the client being out-of-sync and sending - * variable changes for non-existing pids - * - * @return true if successful, false if there was an inconsistency - */ - private boolean handleVariables(WrappedRequest request, - WrappedResponse response, Callback callback, - Application application2, Root root) throws IOException, - InvalidUIDLSecurityKeyException, JSONException { - boolean success = true; - - String changes = getRequestPayload(request); - if (changes != null) { - - // Manage bursts one by one - final String[] bursts = changes.split(String - .valueOf(VAR_BURST_SEPARATOR)); - - // Security: double cookie submission pattern unless disabled by - // property - if (isXSRFEnabled(application2)) { - if (bursts.length == 1 && "init".equals(bursts[0])) { - // init request; don't handle any variables, key sent in - // response. - request.setAttribute(WRITE_SECURITY_TOKEN_FLAG, true); - return true; - } else { - // ApplicationServlet has stored the security token in the - // session; check that it matched the one sent in the UIDL - String sessId = (String) request - .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); - - if (sessId == null || !sessId.equals(bursts[0])) { - throw new InvalidUIDLSecurityKeyException( - "Security key mismatch"); - } - } - - } - - for (int bi = 1; bi < bursts.length; bi++) { - // unescape any encoded separator characters in the burst - final String burst = unescapeBurst(bursts[bi]); - success &= handleBurst(request, root, burst); - - // In case that there were multiple bursts, we know that this is - // a special synchronous case for closing window. Thus we are - // not interested in sending any UIDL changes back to client. - // Still we must clear component tree between bursts to ensure - // that no removed components are updated. The painting after - // the last burst is handled normally by the calling method. - if (bi < bursts.length - 1) { - - // We will be discarding all changes - final PrintWriter outWriter = new PrintWriter( - new CharArrayWriter()); - - paintAfterVariableChanges(request, response, callback, - true, outWriter, root, false); - - } - - } - } - /* - * Note that we ignore inconsistencies while handling unload request. - * The client can't remove invalid variable changes from the burst, and - * we don't have the required logic implemented on the server side. E.g. - * a component is removed in a previous burst. - */ - return success; - } - - /** - * Processes a message burst received from the client. - * - * A burst can contain any number of RPC calls, including legacy variable - * change calls that are processed separately. - * - * Consecutive changes to the value of the same variable are combined and - * changeVariables() is only called once for them. This preserves the Vaadin - * 6 semantics for components and add-ons that do not use Vaadin 7 RPC - * directly. - * - * @param source - * @param root - * the root receiving the burst - * @param burst - * the content of the burst as a String to be parsed - * @return true if the processing of the burst was successful and there were - * no messages to non-existent components - */ - public boolean handleBurst(WrappedRequest source, Root root, - final String burst) { - boolean success = true; - try { - Set<Connector> enabledConnectors = new HashSet<Connector>(); - - List<MethodInvocation> invocations = parseInvocations( - root.getConnectorTracker(), burst); - for (MethodInvocation invocation : invocations) { - final ClientConnector connector = getConnector(root, - invocation.getConnectorId()); - - if (connector != null && connector.isConnectorEnabled()) { - enabledConnectors.add(connector); - } - } - - for (int i = 0; i < invocations.size(); i++) { - MethodInvocation invocation = invocations.get(i); - - final ClientConnector connector = getConnector(root, - invocation.getConnectorId()); - - if (connector == null) { - getLogger().log( - Level.WARNING, - "RPC call to " + invocation.getInterfaceName() - + "." + invocation.getMethodName() - + " received for connector " - + invocation.getConnectorId() - + " but no such connector could be found"); - continue; - } - - if (!enabledConnectors.contains(connector)) { - - if (invocation instanceof LegacyChangeVariablesInvocation) { - LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; - // TODO convert window close to a separate RPC call and - // handle above - not a variable change - - // Handle special case where window-close is called - // after the window has been removed from the - // application or the application has closed - Map<String, Object> changes = legacyInvocation - .getVariableChanges(); - if (changes.size() == 1 && changes.containsKey("close") - && Boolean.TRUE.equals(changes.get("close"))) { - // Silently ignore this - continue; - } - } - - // Connector is disabled, log a warning and move to the next - String msg = "Ignoring RPC call for disabled connector " - + connector.getClass().getName(); - if (connector instanceof Component) { - String caption = ((Component) connector).getCaption(); - if (caption != null) { - msg += ", caption=" + caption; - } - } - getLogger().warning(msg); - continue; - } - - if (invocation instanceof ServerRpcMethodInvocation) { - try { - ServerRpcManager.applyInvocation(connector, - (ServerRpcMethodInvocation) invocation); - } catch (RpcInvocationException e) { - Throwable realException = e.getCause(); - Component errorComponent = null; - if (connector instanceof Component) { - errorComponent = (Component) connector; - } - handleChangeVariablesError(root.getApplication(), - errorComponent, realException, null); - } - } else { - - // All code below is for legacy variable changes - LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; - Map<String, Object> changes = legacyInvocation - .getVariableChanges(); - try { - if (connector instanceof VariableOwner) { - changeVariables(source, (VariableOwner) connector, - changes); - } else { - throw new IllegalStateException( - "Received legacy variable change for " - + connector.getClass().getName() - + " (" - + connector.getConnectorId() - + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: " - + changes.keySet()); - } - } catch (Exception e) { - Component errorComponent = null; - if (connector instanceof Component) { - errorComponent = (Component) connector; - } else if (connector instanceof DragAndDropService) { - Object dropHandlerOwner = changes.get("dhowner"); - if (dropHandlerOwner instanceof Component) { - errorComponent = (Component) dropHandlerOwner; - } - } - handleChangeVariablesError(root.getApplication(), - errorComponent, e, changes); - } - } - } - } catch (JSONException e) { - getLogger().warning( - "Unable to parse RPC call from the client: " - + e.getMessage()); - // TODO or return success = false? - throw new RuntimeException(e); - } - - return success; - } - - /** - * Parse a message burst from the client into a list of MethodInvocation - * instances. - * - * @param connectorTracker - * The ConnectorTracker used to lookup connectors - * @param burst - * message string (JSON) - * @return list of MethodInvocation to perform - * @throws JSONException - */ - private List<MethodInvocation> parseInvocations( - ConnectorTracker connectorTracker, final String burst) - throws JSONException { - JSONArray invocationsJson = new JSONArray(burst); - - ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); - - MethodInvocation previousInvocation = null; - // parse JSON to MethodInvocations - for (int i = 0; i < invocationsJson.length(); ++i) { - - JSONArray invocationJson = invocationsJson.getJSONArray(i); - - MethodInvocation invocation = parseInvocation(invocationJson, - previousInvocation, connectorTracker); - if (invocation != null) { - // Can be null iff the invocation was a legacy invocation and it - // was merged with the previous one - invocations.add(invocation); - previousInvocation = invocation; - } - } - return invocations; - } - - private MethodInvocation parseInvocation(JSONArray invocationJson, - MethodInvocation previousInvocation, - ConnectorTracker connectorTracker) throws JSONException { - String connectorId = invocationJson.getString(0); - String interfaceName = invocationJson.getString(1); - String methodName = invocationJson.getString(2); - - JSONArray parametersJson = invocationJson.getJSONArray(3); - - if (LegacyChangeVariablesInvocation.isLegacyVariableChange( - interfaceName, methodName)) { - if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) { - previousInvocation = null; - } - - return parseLegacyChangeVariablesInvocation(connectorId, - interfaceName, methodName, - (LegacyChangeVariablesInvocation) previousInvocation, - parametersJson, connectorTracker); - } else { - return parseServerRpcInvocation(connectorId, interfaceName, - methodName, parametersJson, connectorTracker); - } - - } - - private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation( - String connectorId, String interfaceName, String methodName, - LegacyChangeVariablesInvocation previousInvocation, - JSONArray parametersJson, ConnectorTracker connectorTracker) - throws JSONException { - if (parametersJson.length() != 2) { - throw new JSONException( - "Invalid parameters in legacy change variables call. Expected 2, was " - + parametersJson.length()); - } - String variableName = parametersJson.getString(0); - UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType( - UidlValue.class, true, parametersJson.get(1), connectorTracker); - - Object value = uidlValue.getValue(); - - if (previousInvocation != null - && previousInvocation.getConnectorId().equals(connectorId)) { - previousInvocation.setVariableChange(variableName, value); - return null; - } else { - return new LegacyChangeVariablesInvocation(connectorId, - variableName, value); - } - } - - private ServerRpcMethodInvocation parseServerRpcInvocation( - String connectorId, String interfaceName, String methodName, - JSONArray parametersJson, ConnectorTracker connectorTracker) - throws JSONException { - ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( - connectorId, interfaceName, methodName, parametersJson.length()); - - Object[] parameters = new Object[parametersJson.length()]; - Type[] declaredRpcMethodParameterTypes = invocation.getMethod() - .getGenericParameterTypes(); - - for (int j = 0; j < parametersJson.length(); ++j) { - Object parameterValue = parametersJson.get(j); - Type parameterType = declaredRpcMethodParameterTypes[j]; - parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType, - parameterValue, connectorTracker); - } - invocation.setParameters(parameters); - return invocation; - } - - protected void changeVariables(Object source, final VariableOwner owner, - Map<String, Object> m) { - owner.changeVariables(source, m); - } - - protected ClientConnector getConnector(Root root, String connectorId) { - ClientConnector c = root.getConnectorTracker() - .getConnector(connectorId); - if (c == null - && connectorId.equals(getDragAndDropService().getConnectorId())) { - return getDragAndDropService(); - } - - return c; - } - - private DragAndDropService getDragAndDropService() { - if (dragAndDropService == null) { - dragAndDropService = new DragAndDropService(this); - } - return dragAndDropService; - } - - /** - * Reads the request data from the Request and returns it converted to an - * UTF-8 string. - * - * @param request - * @return - * @throws IOException - */ - protected String getRequestPayload(WrappedRequest request) - throws IOException { - - int requestLength = request.getContentLength(); - if (requestLength == 0) { - return null; - } - - ByteArrayOutputStream bout = requestLength <= 0 ? new ByteArrayOutputStream() - : new ByteArrayOutputStream(requestLength); - - InputStream inputStream = request.getInputStream(); - byte[] buffer = new byte[MAX_BUFFER_SIZE]; - - while (true) { - int read = inputStream.read(buffer); - if (read == -1) { - break; - } - bout.write(buffer, 0, read); - } - String result = new String(bout.toByteArray(), "utf-8"); - - return result; - } - - public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable { - private final Throwable throwable; - - public ErrorHandlerErrorEvent(Throwable throwable) { - this.throwable = throwable; - } - - @Override - public Throwable getThrowable() { - return throwable; - } - - } - - /** - * Handles an error (exception) that occurred when processing variable - * changes from the client or a failure of a file upload. - * - * For {@link AbstractField} components, - * {@link AbstractField#handleError(com.vaadin.ui.AbstractComponent.ComponentErrorEvent)} - * is called. In all other cases (or if the field does not handle the - * error), {@link ErrorListener#terminalError(ErrorEvent)} for the - * application error handler is called. - * - * @param application - * @param owner - * component that the error concerns - * @param e - * exception that occurred - * @param m - * map from variable names to values - */ - private void handleChangeVariablesError(Application application, - Component owner, Throwable t, Map<String, Object> m) { - boolean handled = false; - ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent( - owner, t, m); - - if (owner instanceof AbstractField) { - try { - handled = ((AbstractField<?>) owner).handleError(errorEvent); - } catch (Exception handlerException) { - /* - * If there is an error in the component error handler we pass - * the that error to the application error handler and continue - * processing the actual error - */ - application.getErrorHandler().terminalError( - new ErrorHandlerErrorEvent(handlerException)); - handled = false; - } - } - - if (!handled) { - application.getErrorHandler().terminalError(errorEvent); - } - - } - - /** - * Unescape encoded burst separator characters in a burst received from the - * client. This protects from separator injection attacks. - * - * @param encodedValue - * to decode - * @return decoded value - */ - protected String unescapeBurst(String encodedValue) { - final StringBuilder result = new StringBuilder(); - final StringCharacterIterator iterator = new StringCharacterIterator( - encodedValue); - char character = iterator.current(); - while (character != CharacterIterator.DONE) { - if (VAR_ESCAPE_CHARACTER == character) { - character = iterator.next(); - switch (character) { - case VAR_ESCAPE_CHARACTER + 0x30: - // escaped escape character - result.append(VAR_ESCAPE_CHARACTER); - break; - case VAR_BURST_SEPARATOR + 0x30: - // +0x30 makes these letters for easier reading - result.append((char) (character - 0x30)); - break; - case CharacterIterator.DONE: - // error - throw new RuntimeException( - "Communication error: Unexpected end of message"); - default: - // other escaped character - probably a client-server - // version mismatch - throw new RuntimeException( - "Invalid escaped character from the client - check that the widgetset and server versions match"); - } - } else { - // not a special character - add it to the result as is - result.append(character); - } - character = iterator.next(); - } - return result.toString(); - } - - /** - * Prints the queued (pending) locale definitions to a {@link PrintWriter} - * in a (UIDL) format that can be sent to the client and used there in - * formatting dates, times etc. - * - * @param outWriter - */ - private void printLocaleDeclarations(PrintWriter outWriter) { - /* - * ----------------------------- Sending Locale sensitive date - * ----------------------------- - */ - - // Send locale informations to client - outWriter.print(", \"locales\":["); - for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { - - final Locale l = generateLocale(locales.get(pendingLocalesIndex)); - // Locale name - outWriter.print("{\"name\":\"" + l.toString() + "\","); - - /* - * Month names (both short and full) - */ - final DateFormatSymbols dfs = new DateFormatSymbols(l); - final String[] short_months = dfs.getShortMonths(); - final String[] months = dfs.getMonths(); - outWriter.print("\"smn\":[\"" - + // ShortMonthNames - short_months[0] + "\",\"" + short_months[1] + "\",\"" - + short_months[2] + "\",\"" + short_months[3] + "\",\"" - + short_months[4] + "\",\"" + short_months[5] + "\",\"" - + short_months[6] + "\",\"" + short_months[7] + "\",\"" - + short_months[8] + "\",\"" + short_months[9] + "\",\"" - + short_months[10] + "\",\"" + short_months[11] + "\"" - + "],"); - outWriter.print("\"mn\":[\"" - + // MonthNames - months[0] + "\",\"" + months[1] + "\",\"" + months[2] - + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" - + months[5] + "\",\"" + months[6] + "\",\"" + months[7] - + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" - + months[10] + "\",\"" + months[11] + "\"" + "],"); - - /* - * Weekday names (both short and full) - */ - final String[] short_days = dfs.getShortWeekdays(); - final String[] days = dfs.getWeekdays(); - outWriter.print("\"sdn\":[\"" - + // ShortDayNames - short_days[1] + "\",\"" + short_days[2] + "\",\"" - + short_days[3] + "\",\"" + short_days[4] + "\",\"" - + short_days[5] + "\",\"" + short_days[6] + "\",\"" - + short_days[7] + "\"" + "],"); - outWriter.print("\"dn\":[\"" - + // DayNames - days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" - + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" - + days[7] + "\"" + "],"); - - /* - * First day of week (0 = sunday, 1 = monday) - */ - final Calendar cal = new GregorianCalendar(l); - outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); - - /* - * Date formatting (MM/DD/YYYY etc.) - */ - - DateFormat dateFormat = DateFormat.getDateTimeInstance( - DateFormat.SHORT, DateFormat.SHORT, l); - if (!(dateFormat instanceof SimpleDateFormat)) { - getLogger().warning( - "Unable to get default date pattern for locale " - + l.toString()); - dateFormat = new SimpleDateFormat(); - } - final String df = ((SimpleDateFormat) dateFormat).toPattern(); - - int timeStart = df.indexOf("H"); - if (timeStart < 0) { - timeStart = df.indexOf("h"); - } - final int ampm_first = df.indexOf("a"); - // E.g. in Korean locale AM/PM is before h:mm - // TODO should take that into consideration on client-side as well, - // now always h:mm a - if (ampm_first > 0 && ampm_first < timeStart) { - timeStart = ampm_first; - } - // Hebrew locale has time before the date - final boolean timeFirst = timeStart == 0; - String dateformat; - if (timeFirst) { - int dateStart = df.indexOf(' '); - if (ampm_first > dateStart) { - dateStart = df.indexOf(' ', ampm_first); - } - dateformat = df.substring(dateStart + 1); - } else { - dateformat = df.substring(0, timeStart - 1); - } - - outWriter.print("\"df\":\"" + dateformat.trim() + "\","); - - /* - * Time formatting (24 or 12 hour clock and AM/PM suffixes) - */ - final String timeformat = df.substring(timeStart, df.length()); - /* - * Doesn't return second or milliseconds. - * - * We use timeformat to determine 12/24-hour clock - */ - final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; - // TODO there are other possibilities as well, like 'h' in french - // (ignore them, too complicated) - final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." - : ":"; - // outWriter.print("\"tf\":\"" + timeformat + "\","); - outWriter.print("\"thc\":" + twelve_hour_clock + ","); - outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\""); - if (twelve_hour_clock) { - final String[] ampm = dfs.getAmPmStrings(); - outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] - + "\"]"); - } - outWriter.print("}"); - if (pendingLocalesIndex < locales.size() - 1) { - outWriter.print(","); - } - } - outWriter.print("]"); // Close locales - } - - /** - * Ends the Application. - * - * The browser is redirected to the Application logout URL set with - * {@link Application#setLogoutURL(String)}, or to the application URL if no - * logout URL is given. - * - * @param request - * the request instance. - * @param response - * the response to write to. - * @param application - * the Application to end. - * @throws IOException - * if the writing failed due to input/output error. - */ - private void endApplication(WrappedRequest request, - WrappedResponse response, Application application) - throws IOException { - - String logoutUrl = application.getLogoutURL(); - if (logoutUrl == null) { - logoutUrl = application.getURL().toString(); - } - // clients JS app is still running, send a special json file to tell - // client that application has quit and where to point browser now - // Set the response type - final OutputStream out = response.getOutputStream(); - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - openJsonMessage(outWriter, response); - outWriter.print("\"redirect\":{"); - outWriter.write("\"url\":\"" + logoutUrl + "\"}"); - closeJsonMessage(outWriter); - outWriter.flush(); - outWriter.close(); - out.flush(); - } - - protected void closeJsonMessage(PrintWriter outWriter) { - outWriter.print("}]"); - } - - /** - * Writes the opening of JSON message to be sent to client. - * - * @param outWriter - * @param response - */ - protected void openJsonMessage(PrintWriter outWriter, - WrappedResponse response) { - // Sets the response type - response.setContentType("application/json; charset=UTF-8"); - // some dirt to prevent cross site scripting - outWriter.print("for(;;);[{"); - } - - /** - * Returns dirty components which are in given window. Components in an - * invisible subtrees are omitted. - * - * @param w - * root window for which dirty components is to be fetched - * @return - */ - private ArrayList<ClientConnector> getDirtyVisibleConnectors( - ConnectorTracker connectorTracker) { - ArrayList<ClientConnector> dirtyConnectors = new ArrayList<ClientConnector>(); - for (ClientConnector c : connectorTracker.getDirtyConnectors()) { - if (isVisible(c)) { - dirtyConnectors.add(c); - } - } - - return dirtyConnectors; - } - - /** - * Queues a locale to be sent to the client (browser) for date and time - * entry etc. All locale specific information is derived from server-side - * {@link Locale} instances and sent to the client when needed, eliminating - * the need to use the {@link Locale} class and all the framework behind it - * on the client. - * - * @see Locale#toString() - * - * @param value - */ - public void requireLocale(String value) { - if (locales == null) { - locales = new ArrayList<String>(); - locales.add(application.getLocale().toString()); - pendingLocalesIndex = 0; - } - if (!locales.contains(value)) { - locales.add(value); - } - } - - /** - * Constructs a {@link Locale} instance to be sent to the client based on a - * short locale description string. - * - * @see #requireLocale(String) - * - * @param value - * @return - */ - private Locale generateLocale(String value) { - final String[] temp = value.split("_"); - if (temp.length == 1) { - return new Locale(temp[0]); - } else if (temp.length == 2) { - return new Locale(temp[0], temp[1]); - } else { - return new Locale(temp[0], temp[1], temp[2]); - } - } - - protected class InvalidUIDLSecurityKeyException extends - GeneralSecurityException { - - InvalidUIDLSecurityKeyException(String message) { - super(message); - } - - } - - private final HashMap<Class<? extends ClientConnector>, Integer> typeToKey = new HashMap<Class<? extends ClientConnector>, Integer>(); - private int nextTypeKey = 0; - - private BootstrapHandler bootstrapHandler; - - String getTagForType(Class<? extends ClientConnector> class1) { - Integer id = typeToKey.get(class1); - if (id == null) { - id = nextTypeKey++; - typeToKey.put(class1, id); - getLogger().log(Level.FINE, - "Mapping " + class1.getName() + " to " + id); - } - return id.toString(); - } - - /** - * Helper class for terminal to keep track of data that client is expected - * to know. - * - * TODO make customlayout templates (from theme) to be cached here. - */ - class ClientCache implements Serializable { - - private final Set<Object> res = new HashSet<Object>(); - - /** - * - * @param paintable - * @return true if the given class was added to cache - */ - boolean cache(Object object) { - return res.add(object); - } - - public void clear() { - res.clear(); - } - - } - - public String getStreamVariableTargetUrl(ClientConnector owner, - String name, StreamVariable value) { - /* - * We will use the same APP/* URI space as ApplicationResources but - * prefix url with UPLOAD - * - * eg. APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] - * - * SECKEY is created on each paint to make URL's unpredictable (to - * prevent CSRF attacks). - * - * NAME and PID from URI forms a key to fetch StreamVariable when - * handling post - */ - String paintableId = owner.getConnectorId(); - int rootId = owner.getRoot().getRootId(); - String key = rootId + "/" + paintableId + "/" + name; - - if (pidToNameToStreamVariable == null) { - pidToNameToStreamVariable = new HashMap<String, Map<String, StreamVariable>>(); - } - Map<String, StreamVariable> nameToStreamVariable = pidToNameToStreamVariable - .get(paintableId); - if (nameToStreamVariable == null) { - nameToStreamVariable = new HashMap<String, StreamVariable>(); - pidToNameToStreamVariable.put(paintableId, nameToStreamVariable); - } - nameToStreamVariable.put(name, value); - - if (streamVariableToSeckey == null) { - streamVariableToSeckey = new HashMap<StreamVariable, String>(); - } - String seckey = streamVariableToSeckey.get(value); - if (seckey == null) { - seckey = UUID.randomUUID().toString(); - streamVariableToSeckey.put(value, seckey); - } - - return ApplicationConnection.APP_PROTOCOL_PREFIX - + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey; - - } - - public void cleanStreamVariable(ClientConnector owner, String name) { - Map<String, StreamVariable> nameToStreamVar = pidToNameToStreamVariable - .get(owner.getConnectorId()); - nameToStreamVar.remove(name); - if (nameToStreamVar.isEmpty()) { - pidToNameToStreamVariable.remove(owner.getConnectorId()); - } - } - - /** - * Gets the bootstrap handler that should be used for generating the pages - * bootstrapping applications for this communication manager. - * - * @return the bootstrap handler to use - */ - private BootstrapHandler getBootstrapHandler() { - if (bootstrapHandler == null) { - bootstrapHandler = createBootstrapHandler(); - } - - return bootstrapHandler; - } - - protected abstract BootstrapHandler createBootstrapHandler(); - - protected boolean handleApplicationRequest(WrappedRequest request, - WrappedResponse response) throws IOException { - return application.handleRequest(request, response); - } - - public void handleBrowserDetailsRequest(WrappedRequest request, - WrappedResponse response, Application application) - throws IOException { - - // if we do not yet have a currentRoot, it should be initialized - // shortly, and we should send the initial UIDL - boolean sendUIDL = Root.getCurrent() == null; - - try { - CombinedRequest combinedRequest = new CombinedRequest(request); - - Root root = application.getRootForRequest(combinedRequest); - response.setContentType("application/json; charset=UTF-8"); - - // Use the same logic as for determined roots - BootstrapHandler bootstrapHandler = getBootstrapHandler(); - BootstrapContext context = bootstrapHandler.createContext( - combinedRequest, response, application, root.getRootId()); - - String widgetset = context.getWidgetsetName(); - String theme = context.getThemeName(); - String themeUri = bootstrapHandler.getThemeUri(context, theme); - - // TODO These are not required if it was only the init of the root - // that was delayed - JSONObject params = new JSONObject(); - params.put("widgetset", widgetset); - params.put("themeUri", themeUri); - // Root id might have changed based on e.g. window.name - params.put(ApplicationConnection.ROOT_ID_PARAMETER, - root.getRootId()); - if (sendUIDL) { - String initialUIDL = getInitialUIDL(combinedRequest, root); - params.put("uidl", initialUIDL); - } - - // NOTE! GateIn requires, for some weird reason, getOutputStream - // to be used instead of getWriter() (it seems to interpret - // application/json as a binary content type) - final OutputStream out = response.getOutputStream(); - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - - outWriter.write(params.toString()); - // NOTE GateIn requires the buffers to be flushed to work - outWriter.flush(); - out.flush(); - } catch (RootRequiresMoreInformationException e) { - // Requiring more information at this point is not allowed - // TODO handle in a better way - throw new RuntimeException(e); - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - /** - * Generates the initial UIDL message that can e.g. be included in a html - * page to avoid a separate round trip just for getting the UIDL. - * - * @param request - * the request that caused the initialization - * @param root - * the root for which the UIDL should be generated - * @return a string with the initial UIDL message - * @throws PaintException - * if an exception occurs while painting - * @throws JSONException - * if an exception occurs while encoding output - */ - protected String getInitialUIDL(WrappedRequest request, Root root) - throws PaintException, JSONException { - // TODO maybe unify writeUidlResponse()? - StringWriter sWriter = new StringWriter(); - PrintWriter pWriter = new PrintWriter(sWriter); - pWriter.print("{"); - if (isXSRFEnabled(root.getApplication())) { - pWriter.print(getSecurityKeyUIDL(request)); - } - writeUidlResponse(request, true, pWriter, root, false); - pWriter.print("}"); - String initialUIDL = sWriter.toString(); - getLogger().log(Level.FINE, "Initial UIDL:" + initialUIDL); - return initialUIDL; - } - - /** - * Serve a connector resource from the classpath if the resource has - * previously been registered by calling - * {@link #registerResource(String, Class)}. Sending arbitrary files from - * the classpath is prevented by only accepting resource names that have - * explicitly been registered. Resources can currently only be registered by - * including a {@link JavaScript} or {@link StyleSheet} annotation on a - * Connector class. - * - * @param request - * @param response - * - * @throws IOException - */ - public void serveConnectorResource(WrappedRequest request, - WrappedResponse response) throws IOException { - - String pathInfo = request.getRequestPathInfo(); - // + 2 to also remove beginning and ending slashes - String resourceName = pathInfo - .substring(ApplicationConnection.CONNECTOR_RESOURCE_PREFIX - .length() + 2); - - final String mimetype = response.getDeploymentConfiguration() - .getMimeType(resourceName); - - // Security check: avoid accidentally serving from the root of the - // classpath instead of relative to the context class - if (resourceName.startsWith("/")) { - getLogger().warning( - "Connector resource request starting with / rejected: " - + resourceName); - response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); - return; - } - - // Check that the resource name has been registered - Class<?> context; - synchronized (connectorResourceContexts) { - context = connectorResourceContexts.get(resourceName); - } - - // Security check: don't serve resource if the name hasn't been - // registered in the map - if (context == null) { - getLogger().warning( - "Connector resource request for unknown resource rejected: " - + resourceName); - response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); - return; - } - - // Resolve file relative to the location of the context class - InputStream in = context.getResourceAsStream(resourceName); - if (in == null) { - getLogger().warning( - resourceName + " defined by " + context.getName() - + " not found. Verify that the file " - + context.getPackage().getName().replace('.', '/') - + '/' + resourceName - + " is available on the classpath."); - response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName); - return; - } - - // TODO Check and set cache headers - - OutputStream out = null; - try { - if (mimetype != null) { - response.setContentType(mimetype); - } - - out = response.getOutputStream(); - - final byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE]; - - int bytesRead = 0; - while ((bytesRead = in.read(buffer)) > 0) { - out.write(buffer, 0, bytesRead); - } - out.flush(); - } finally { - try { - in.close(); - } catch (Exception e) { - // Do nothing - } - if (out != null) { - try { - out.close(); - } catch (Exception e) { - // Do nothing - } - } - } - } - - /** - * Handles file upload request submitted via Upload component. - * - * @param root - * The root for this request - * - * @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable) - * - * @param request - * @param response - * @throws IOException - * @throws InvalidUIDLSecurityKeyException - */ - public void handleFileUpload(Application application, - WrappedRequest request, WrappedResponse response) - throws IOException, InvalidUIDLSecurityKeyException { - - /* - * URI pattern: APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] See - * #createReceiverUrl - */ - - String pathInfo = request.getRequestPathInfo(); - // strip away part until the data we are interested starts - int startOfData = pathInfo - .indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX) - + ServletPortletHelper.UPLOAD_URL_PREFIX.length(); - String uppUri = pathInfo.substring(startOfData); - String[] parts = uppUri.split("/", 4); // 0= rootid, 1 = cid, 2= name, 3 - // = sec key - String rootId = parts[0]; - String connectorId = parts[1]; - String variableName = parts[2]; - Root root = application.getRootById(Integer.parseInt(rootId)); - Root.setCurrent(root); - - StreamVariable streamVariable = getStreamVariable(connectorId, - variableName); - String secKey = streamVariableToSeckey.get(streamVariable); - if (secKey.equals(parts[3])) { - - ClientConnector source = getConnector(root, connectorId); - String contentType = request.getContentType(); - if (contentType.contains("boundary")) { - // Multipart requests contain boundary string - doHandleSimpleMultipartFileUpload(request, response, - streamVariable, variableName, source, - contentType.split("boundary=")[1]); - } else { - // if boundary string does not exist, the posted file is from - // XHR2.post(File) - doHandleXhrFilePost(request, response, streamVariable, - variableName, source, request.getContentLength()); - } - } else { - throw new InvalidUIDLSecurityKeyException( - "Security key in upload post did not match!"); - } - - } - - public StreamVariable getStreamVariable(String connectorId, - String variableName) { - Map<String, StreamVariable> map = pidToNameToStreamVariable - .get(connectorId); - if (map == null) { - return null; - } - StreamVariable streamVariable = map.get(variableName); - return streamVariable; - } - - /** - * Stream that extracts content from another stream until the boundary - * string is encountered. - * - * Public only for unit tests, should be considered private for all other - * purposes. - */ - public static class SimpleMultiPartInputStream extends InputStream { - - /** - * Counter of how many characters have been matched to boundary string - * from the stream - */ - int matchedCount = -1; - - /** - * Used as pointer when returning bytes after partly matched boundary - * string. - */ - int curBoundaryIndex = 0; - /** - * The byte found after a "promising start for boundary" - */ - private int bufferedByte = -1; - private boolean atTheEnd = false; - - private final char[] boundary; - - private final InputStream realInputStream; - - public SimpleMultiPartInputStream(InputStream realInputStream, - String boundaryString) { - boundary = (CRLF + DASHDASH + boundaryString).toCharArray(); - this.realInputStream = realInputStream; - } - - @Override - public int read() throws IOException { - if (atTheEnd) { - // End boundary reached, nothing more to read - return -1; - } else if (bufferedByte >= 0) { - /* Purge partially matched boundary if there was such */ - return getBuffered(); - } else if (matchedCount != -1) { - /* - * Special case where last "failed" matching ended with first - * character from boundary string - */ - return matchForBoundary(); - } else { - int fromActualStream = realInputStream.read(); - if (fromActualStream == -1) { - // unexpected end of stream - throw new IOException( - "The multipart stream ended unexpectedly"); - } - if (boundary[0] == fromActualStream) { - /* - * If matches the first character in boundary string, start - * checking if the boundary is fetched. - */ - return matchForBoundary(); - } - return fromActualStream; - } - } - - /** - * Reads the input to expect a boundary string. Expects that the first - * character has already been matched. - * - * @return -1 if the boundary was matched, else returns the first byte - * from boundary - * @throws IOException - */ - private int matchForBoundary() throws IOException { - matchedCount = 0; - /* - * Going to "buffered mode". Read until full boundary match or a - * different character. - */ - while (true) { - matchedCount++; - if (matchedCount == boundary.length) { - /* - * The whole boundary matched so we have reached the end of - * file - */ - atTheEnd = true; - return -1; - } - int fromActualStream = realInputStream.read(); - if (fromActualStream != boundary[matchedCount]) { - /* - * Did not find full boundary, cache the mismatching byte - * and start returning the partially matched boundary. - */ - bufferedByte = fromActualStream; - return getBuffered(); - } - } - } - - /** - * Returns the partly matched boundary string and the byte following - * that. - * - * @return - * @throws IOException - */ - private int getBuffered() throws IOException { - int b; - if (matchedCount == 0) { - // The boundary has been returned, return the buffered byte. - b = bufferedByte; - bufferedByte = -1; - matchedCount = -1; - } else { - b = boundary[curBoundaryIndex++]; - if (curBoundaryIndex == matchedCount) { - // The full boundary has been returned, remaining is the - // char that did not match the boundary. - - curBoundaryIndex = 0; - if (bufferedByte != boundary[0]) { - /* - * next call for getBuffered will return the - * bufferedByte that came after the partial boundary - * match - */ - matchedCount = 0; - } else { - /* - * Special case where buffered byte again matches the - * boundaryString. This could be the start of the real - * end boundary. - */ - matchedCount = 0; - bufferedByte = -1; - } - } - } - if (b == -1) { - throw new IOException("The multipart stream ended unexpectedly"); - } - return b; - } - } - - private static final Logger getLogger() { - return Logger.getLogger(AbstractCommunicationManager.class.getName()); - } -} |