/* @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.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.client.Connector; import com.vaadin.terminal.gwt.client.communication.MethodInvocation; import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.client.communication.UidlValue; 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 rootToClientCache = 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; private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; /** * The application this communication manager is used for */ private final Application application; private List locales; private int pendingLocalesIndex; private int timeoutInterval = -1; private DragAndDropService dragAndDropService; private String requestThemeName; private int maxInactiveInterval; private Connector highlightedConnector; private Map> connectorResourceContexts = new HashMap>(); /** * 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()); } } 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()); } } 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("download handled"); 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(); } 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 h = new LinkedList(); 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 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 dirtyVisibleConnectors = new ArrayList(); 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 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 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 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()); // } // } 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 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> usedClientConnectors = paintTarget .getUsedClientConnectors(); boolean typeMappingsOpen = false; ClientCache clientCache = getClientCache(root); 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>() { 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 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 registerConnecctorResource(resourceName, context); } if (protocol != null || uri.getHost() != null) { return resourceUri; } // Bare path interpreted as connector resource return registerConnecctorResource(resourceUri, context); } catch (URISyntaxException e) { getLogger().log(Level.WARNING, "Could not parse resource url " + resourceUri, e); return resourceUri; } } private String registerConnecctorResource(String name, Class context) { synchronized (connectorResourceContexts) { // Add to map of names accepted by serveConnectorResource if (connectorResourceContexts.containsKey(name)) { Class oldContext = connectorResourceContexts.get(name); getLogger().warning( "Resource " + name + " defined by both " + context + " and " + oldContext + ". Resource from " + oldContext + " will be used."); } else { connectorResourceContexts.put(name, context); } } return "connector:///" + 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 dirtyVisibleConnectors) throws PaintException { List legacyComponents = new ArrayList(); 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 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() { 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 true if the connector is visible to the client, * false 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 implements Iterator { public boolean hasNext() { return false; } public E next() { return null; } 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 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(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 enabledConnectors = new HashSet(); List 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 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 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 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 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 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; } 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 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 getDirtyVisibleConnectors( ConnectorTracker connectorTracker) { ArrayList dirtyConnectors = new ArrayList(); 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(); 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, Integer> typeToKey = new HashMap, Integer>(); private int nextTypeKey = 0; private BootstrapHandler bootstrapHandler; String getTagForType(Class 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 res = new HashSet(); /** * * @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(); } } abstract String getStreamVariableTargetUrl(ClientConnector owner, String name, StreamVariable value); abstract protected void cleanStreamVariable(ClientConnector owner, String name); /** * 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 resourceName * @param request * @param response * @param mimetype * @throws IOException */ public void serveConnectorResource(String resourceName, WrappedRequest request, WrappedResponse response, String mimetype) throws IOException { // 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 unkown 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 } } } } /** * 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()); } }