/* * Copyright 2011 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.terminal.gwt.client; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONString; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ComponentState; import com.vaadin.shared.Version; import com.vaadin.shared.communication.MethodInvocation; import com.vaadin.shared.communication.SharedState; import com.vaadin.shared.communication.UidlValue; import com.vaadin.terminal.gwt.client.ApplicationConfiguration.ErrorMessage; import com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener; import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper; import com.vaadin.terminal.gwt.client.communication.JsonDecoder; import com.vaadin.terminal.gwt.client.communication.JsonEncoder; import com.vaadin.terminal.gwt.client.communication.RpcManager; import com.vaadin.terminal.gwt.client.communication.SerializerMap; import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; import com.vaadin.terminal.gwt.client.communication.Type; import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector; import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; import com.vaadin.terminal.gwt.client.ui.VContextMenu; import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; import com.vaadin.terminal.gwt.client.ui.notification.VNotification; import com.vaadin.terminal.gwt.client.ui.notification.VNotification.HideEvent; import com.vaadin.terminal.gwt.client.ui.root.RootConnector; import com.vaadin.terminal.gwt.client.ui.window.WindowConnector; /** * This is the client side communication "engine", managing client-server * communication with its server side counterpart * com.vaadin.terminal.gwt.server.AbstractCommunicationManager. * * Client-side connectors receive updates from the corresponding server-side * connector (typically component) as state updates or RPC calls. The connector * has the possibility to communicate back with its server side counter part * through RPC calls. * * TODO document better * * Entry point classes (widgetsets) define onModuleLoad(). */ public class ApplicationConnection { public static final String MODIFIED_CLASSNAME = "v-modified"; public static final String DISABLED_CLASSNAME = "v-disabled"; public static final String REQUIRED_CLASSNAME_EXT = "-required"; public static final String ERROR_CLASSNAME_EXT = "-error"; public static final char VAR_BURST_SEPARATOR = '\u001d'; public static final char VAR_ESCAPE_CHARACTER = '\u001b'; private static SerializerMap serializerMap; /** * A string that, if found in a non-JSON response to a UIDL request, will * cause the browser to refresh the page. If followed by a colon, optional * whitespace, and a URI, causes the browser to synchronously load the URI. * *

* This allows, for instance, a servlet filter to redirect the application * to a custom login page when the session expires. For example: *

* *
     * if (sessionExpired) {
     *     response.setHeader("Content-Type", "text/html");
     *     response.getWriter().write(
     *             myLoginPageHtml + "<!-- Vaadin-Refresh: "
     *                     + request.getContextPath() + " -->");
     * }
     * 
*/ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; // will hold the UIDL security key (for XSS protection) once received private String uidlSecurityKey = "init"; private final HashMap resourcesMap = new HashMap(); private ArrayList pendingInvocations = new ArrayList(); private WidgetSet widgetSet; private VContextMenu contextMenu = null; private Timer loadTimer; private Timer loadTimer2; private Timer loadTimer3; private Element loadElement; private final RootConnector rootConnector; protected boolean applicationRunning = false; private boolean hasActiveRequest = false; protected boolean cssLoaded = false; /** Parameters for this application connection loaded from the web-page */ private ApplicationConfiguration configuration; /** List of pending variable change bursts that must be submitted in order */ private final ArrayList> pendingBursts = new ArrayList>(); /** Timer for automatic refirect to SessionExpiredURL */ private Timer redirectTimer; /** redirectTimer scheduling interval in seconds */ private int sessionExpirationInterval; private ArrayList componentCaptionSizeChanges = new ArrayList(); private Date requestStartTime; private boolean validatingLayouts = false; private Set zeroWidthComponents = null; private Set zeroHeightComponents = null; private final LayoutManager layoutManager; private final RpcManager rpcManager; public static class MultiStepDuration extends Duration { private int previousStep = elapsedMillis(); public void logDuration(String message) { logDuration(message, 0); } public void logDuration(String message, int minDuration) { int currentTime = elapsedMillis(); int stepDuration = currentTime - previousStep; if (stepDuration >= minDuration) { VConsole.log(message + ": " + stepDuration + " ms"); } previousStep = currentTime; } } public ApplicationConnection() { rootConnector = GWT.create(RootConnector.class); rpcManager = GWT.create(RpcManager.class); layoutManager = GWT.create(LayoutManager.class); layoutManager.setConnection(this); serializerMap = GWT.create(SerializerMap.class); } public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { VConsole.log("Starting application " + cnf.getRootPanelId()); VConsole.log("Vaadin application servlet version: " + cnf.getServletVersion()); VConsole.log("Application version: " + cnf.getApplicationVersion()); if (!cnf.getServletVersion().equals(Version.getFullVersion())) { VConsole.error("Warning: your widget set seems to be built with a different " + "version than the one used on server. Unexpected " + "behavior may occur."); } this.widgetSet = widgetSet; configuration = cnf; ComponentLocator componentLocator = new ComponentLocator(this); String appRootPanelName = cnf.getRootPanelId(); // remove the end (window name) of autogenerated rootpanel id appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", ""); initializeTestbenchHooks(componentLocator, appRootPanelName); initializeClientHooks(); rootConnector.init(cnf.getRootPanelId(), this); showLoadingIndicator(); } /** * Starts this application. Don't call this method directly - it's called by * {@link ApplicationConfiguration#startNextApplication()}, which should be * called once this application has started (first response received) or * failed to start. This ensures that the applications are started in order, * to avoid session-id problems. * */ public void start() { String jsonText = configuration.getUIDL(); if (jsonText == null) { // inital UIDL not in DOM, request later repaintAll(); } else { // Update counter so TestBench knows something is still going on hasActiveRequest = true; // initial UIDL provided in DOM, continue as if returned by request handleJSONText(jsonText, -1); } } private native void initializeTestbenchHooks( ComponentLocator componentLocator, String TTAppId) /*-{ var ap = this; var client = {}; client.isActive = $entry(function() { return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()() || ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()(); }); var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()(); if (vi) { client.getVersionInfo = function() { return vi; } } client.getProfilingData = $entry(function() { var pd = [ ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::lastProcessingTime, ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::totalProcessingTime ]; pd = pd.concat(ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::serverTimingInfo); return pd; }); client.getElementByPath = $entry(function(id) { return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id); }); client.getPathForElement = $entry(function(element) { return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element); }); $wnd.vaadin.clients[TTAppId] = client; }-*/; /** * Helper for tt initialization */ private JavaScriptObject getVersionInfo() { return configuration.getVersionInfoJSObject(); } /** * Publishes a JavaScript API for mash-up applications. *
    *
  • vaadin.forceSync() sends pending variable changes, in * effect synchronizing the server and client state. This is done for all * applications on host page.
  • *
  • vaadin.postRequestHooks is a map of functions which gets * called after each XHR made by vaadin application. Note, that it is * attaching js functions responsibility to create the variable like this: * *
         * if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();}
         * postRequestHooks.myHook = function(appId) {
         *          if(appId == "MyAppOfInterest") {
         *                  // do the staff you need on xhr activity
         *          }
         * }
         * 
    First parameter passed to these functions is the identifier * of Vaadin application that made the request. *
* * TODO make this multi-app aware */ private native void initializeClientHooks() /*-{ var app = this; var oldSync; if ($wnd.vaadin.forceSync) { oldSync = $wnd.vaadin.forceSync; } $wnd.vaadin.forceSync = $entry(function() { if (oldSync) { oldSync(); } app.@com.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()(); }); var oldForceLayout; if ($wnd.vaadin.forceLayout) { oldForceLayout = $wnd.vaadin.forceLayout; } $wnd.vaadin.forceLayout = $entry(function() { if (oldForceLayout) { oldForceLayout(); } app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()(); }); }-*/; /** * Runs possibly registered client side post request hooks. This is expected * to be run after each uidl request made by Vaadin application. * * @param appId */ private static native void runPostRequestHooks(String appId) /*-{ if ($wnd.vaadin.postRequestHooks) { for ( var hook in $wnd.vaadin.postRequestHooks) { if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") { try { $wnd.vaadin.postRequestHooks[hook](appId); } catch (e) { } } } } }-*/; /** * If on Liferay and logged in, ask the client side session management * JavaScript to extend the session duration. * * Otherwise, Liferay client side JavaScript will explicitly expire the * session even though the server side considers the session to be active. * See ticket #8305 for more information. */ protected native void extendLiferaySession() /*-{ if ($wnd.Liferay && $wnd.Liferay.Session) { $wnd.Liferay.Session.extend(); // if the extend banner is visible, hide it if ($wnd.Liferay.Session.banner) { $wnd.Liferay.Session.banner.remove(); } } }-*/; /** * Get the active Console for writing debug messages. May return an actual * logging console, or the NullConsole if debugging is not turned on. * * @deprecated Developers should use {@link VConsole} since 6.4.5 * * @return the active Console */ @Deprecated public static Console getConsole() { return VConsole.getImplementation(); } /** * Checks if client side is in debug mode. Practically this is invoked by * adding ?debug parameter to URI. * * @deprecated use ApplicationConfiguration isDebugMode instead. * * @return true if client side is currently been debugged */ @Deprecated public static boolean isDebugMode() { return ApplicationConfiguration.isDebugMode(); } /** * Gets the application base URI. Using this other than as the download * action URI can cause problems in Portlet 2.0 deployments. * * @return application base URI */ public String getAppUri() { return configuration.getApplicationUri(); }; /** * Indicates whether or not there are currently active UIDL requests. Used * internally to sequence requests properly, seldom needed in Widgets. * * @return true if there are active requests */ public boolean hasActiveRequest() { return hasActiveRequest; } private String getRepaintAllParameters() { // collect some client side data that will be sent to server on // initial uidl request String nativeBootstrapParameters = getNativeBrowserDetailsParameters(getConfiguration() .getRootPanelId()); // TODO figure out how client and view size could be used better on // server. screen size can be accessed via Browser object, but other // values currently only via transaction listener. String parameters = "repaintAll=1&" + nativeBootstrapParameters; return parameters; } /** * Gets the browser detail parameters that are sent by the bootstrap * javascript for two-request initialization. * * @param parentElementId * @return */ private static native String getNativeBrowserDetailsParameters( String parentElementId) /*-{ return $wnd.vaadin.getBrowserDetailsParameters(parentElementId); }-*/; protected void repaintAll() { String repainAllParameters = getRepaintAllParameters(); makeUidlRequest("", repainAllParameters, false); } /** * Requests an analyze of layouts, to find inconsistencies. Exclusively used * for debugging during development. */ public void analyzeLayouts() { String params = getRepaintAllParameters() + "&analyzeLayouts=1"; makeUidlRequest("", params, false); } /** * Sends a request to the server to print details to console that will help * developer to locate component in the source code. * * @param componentConnector */ void highlightComponent(ComponentConnector componentConnector) { String params = getRepaintAllParameters() + "&highlightComponent=" + componentConnector.getConnectorId(); makeUidlRequest("", params, false); } /** * Makes an UIDL request to the server. * * @param requestData * Data that is passed to the server. * @param extraParams * Parameters that are added as GET parameters to the url. * Contains key=value pairs joined by & characters or is empty if * no parameters should be added. Should not start with any * special character. * @param forceSync * true if the request should be synchronous, false otherwise */ protected void makeUidlRequest(final String requestData, final String extraParams, final boolean forceSync) { startRequest(); // Security: double cookie submission pattern final String payload = uidlSecurityKey + VAR_BURST_SEPARATOR + requestData; VConsole.log("Making UIDL Request with params: " + payload); String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + ApplicationConstants.UIDL_REQUEST_PATH); if (extraParams != null && extraParams.length() > 0) { uri = addGetParameters(uri, extraParams); } uri = addGetParameters(uri, ApplicationConstants.ROOT_ID_PARAMETER + "=" + configuration.getRootId()); doUidlRequest(uri, payload, forceSync); } /** * Sends an asynchronous or synchronous UIDL request to the server using the * given URI. * * @param uri * The URI to use for the request. May includes GET parameters * @param payload * The contents of the request to send * @param synchronous * true if the request should be synchronous, false otherwise */ protected void doUidlRequest(final String uri, final String payload, final boolean synchronous) { if (!synchronous) { RequestCallback requestCallback = new RequestCallback() { @Override public void onError(Request request, Throwable exception) { showCommunicationError(exception.getMessage(), -1); endRequest(); } @Override public void onResponseReceived(Request request, Response response) { VConsole.log("Server visit took " + String.valueOf((new Date()).getTime() - requestStartTime.getTime()) + "ms"); int statusCode = response.getStatusCode(); switch (statusCode) { case 0: showCommunicationError( "Invalid status code 0 (server down?)", statusCode); endRequest(); return; case 401: /* * Authorization has failed. Could be that the session * has timed out and the container is redirecting to a * login page. */ showAuthenticationError(""); endRequest(); return; case 503: /* * We'll assume msec instead of the usual seconds. If * there's no Retry-After header, handle the error like * a 500, as per RFC 2616 section 10.5.4. */ String delay = response.getHeader("Retry-After"); if (delay != null) { VConsole.log("503, retrying in " + delay + "msec"); (new Timer() { @Override public void run() { doUidlRequest(uri, payload, synchronous); } }).schedule(Integer.parseInt(delay)); return; } } if ((statusCode / 100) == 4) { // Handle all 4xx errors the same way as (they are // all permanent errors) showCommunicationError( "UIDL could not be read from server. Check servlets mappings. Error code: " + statusCode, statusCode); endRequest(); return; } else if ((statusCode / 100) == 5) { // Something's wrong on the server, there's nothing the // client can do except maybe try again. showCommunicationError("Server error. Error code: " + statusCode, statusCode); endRequest(); return; } String contentType = response.getHeader("Content-Type"); if (contentType == null || !contentType.startsWith("application/json")) { /* * A servlet filter or equivalent may have intercepted * the request and served non-UIDL content (for * instance, a login page if the session has expired.) * If the response contains a magic substring, do a * synchronous refresh. See #8241. */ MatchResult refreshToken = RegExp.compile( UIDL_REFRESH_TOKEN + "(:\\s*(.*?))?(\\s|$)") .exec(response.getText()); if (refreshToken != null) { redirect(refreshToken.getGroup(2)); return; } } // for(;;);[realjson] final String jsonText = response.getText().substring(9, response.getText().length() - 1); handleJSONText(jsonText, statusCode); } }; try { doAsyncUIDLRequest(uri, payload, requestCallback); } catch (RequestException e) { VConsole.error(e); endRequest(); } } else { // Synchronized call, discarded response (leaving the page) SynchronousXHR syncXHR = (SynchronousXHR) SynchronousXHR.create(); syncXHR.synchronousPost(uri + "&" + ApplicationConstants.PARAM_UNLOADBURST + "=1", payload); /* * Although we are in theory leaving the page, the page may still * stay open. End request properly here too. See #3289 */ endRequest(); } } /** * Handles received UIDL JSON text, parsing it, and passing it on to the * appropriate handlers, while logging timiing information. * * @param jsonText * @param statusCode */ private void handleJSONText(String jsonText, int statusCode) { final Date start = new Date(); final ValueMap json; try { json = parseJSONResponse(jsonText); } catch (final Exception e) { endRequest(); showCommunicationError(e.getMessage() + " - Original JSON-text:" + jsonText, statusCode); return; } VConsole.log("JSON parsing took " + (new Date().getTime() - start.getTime()) + "ms"); if (applicationRunning) { handleReceivedJSONMessage(start, jsonText, json); } else { applicationRunning = true; handleWhenCSSLoaded(jsonText, json); } } /** * Sends an asynchronous UIDL request to the server using the given URI. * * @param uri * The URI to use for the request. May includes GET parameters * @param payload * The contents of the request to send * @param requestCallback * The handler for the response * @throws RequestException * if the request could not be sent */ protected void doAsyncUIDLRequest(String uri, String payload, RequestCallback requestCallback) throws RequestException { RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); // TODO enable timeout // rb.setTimeoutMillis(timeoutMillis); rb.setHeader("Content-Type", "text/plain;charset=utf-8"); rb.setRequestData(payload); rb.setCallback(requestCallback); rb.send(); } int cssWaits = 0; /** * Holds the time spent rendering the last request */ protected int lastProcessingTime; /** * Holds the total time spent rendering requests during the lifetime of the * session. */ protected int totalProcessingTime; /** * Holds the timing information from the server-side. How much time was * spent servicing the last request and how much time has been spent * servicing the session so far. These values are always one request behind, * since they cannot be measured before the request is finished. */ private ValueMap serverTimingInfo; static final int MAX_CSS_WAITS = 100; protected void handleWhenCSSLoaded(final String jsonText, final ValueMap json) { if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) { (new Timer() { @Override public void run() { handleWhenCSSLoaded(jsonText, json); } }).schedule(50); VConsole.log("Assuming CSS loading is not complete, " + "postponing render phase. " + "(.v-loading-indicator height == 0)"); cssWaits++; } else { cssLoaded = true; handleReceivedJSONMessage(new Date(), jsonText, json); if (cssWaits >= MAX_CSS_WAITS) { VConsole.error("CSS files may have not loaded properly."); } } } /** * Checks whether or not the CSS is loaded. By default checks the size of * the loading indicator element. * * @return */ protected boolean isCSSLoaded() { return cssLoaded || DOM.getElementPropertyInt(loadElement, "offsetHeight") != 0; } /** * Shows the communication error notification. * * @param details * Optional details for debugging. * @param statusCode * The status code returned for the request * */ protected void showCommunicationError(String details, int statusCode) { VConsole.error("Communication error: " + details); ErrorMessage communicationError = configuration.getCommunicationError(); showError(details, communicationError.getCaption(), communicationError.getMessage(), communicationError.getUrl()); } /** * Shows the authentication error notification. * * @param details * Optional details for debugging. */ protected void showAuthenticationError(String details) { VConsole.error("Authentication error: " + details); ErrorMessage authorizationError = configuration.getAuthorizationError(); showError(details, authorizationError.getCaption(), authorizationError.getMessage(), authorizationError.getUrl()); } /** * Shows the error notification. * * @param details * Optional details for debugging. */ private void showError(String details, String caption, String message, String url) { StringBuilder html = new StringBuilder(); if (caption != null) { html.append("

"); html.append(caption); html.append("

"); } if (message != null) { html.append("

"); html.append(message); html.append("

"); } if (html.length() > 0) { // Add error description html.append("

"); html.append(details); html.append("

"); VNotification n = VNotification.createNotification(1000 * 60 * 45); n.addEventListener(new NotificationRedirect(url)); n.show(html.toString(), VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM); } else { redirect(url); } } protected void startRequest() { if (hasActiveRequest) { VConsole.error("Trying to start a new request while another is active"); } hasActiveRequest = true; requestStartTime = new Date(); // show initial throbber if (loadTimer == null) { loadTimer = new Timer() { @Override public void run() { /* * IE7 does not properly cancel the event with * loadTimer.cancel() so we have to check that we really * should make it visible */ if (loadTimer != null) { showLoadingIndicator(); } } }; // First one kicks in at 300ms } loadTimer.schedule(300); } protected void endRequest() { if (!hasActiveRequest) { VConsole.error("No active request"); } // After checkForPendingVariableBursts() there may be a new active // request, so we must set hasActiveRequest to false before, not after, // the call. Active requests used to be tracked with an integer counter, // so setting it after used to work but not with the #8505 changes. hasActiveRequest = false; if (applicationRunning) { checkForPendingVariableBursts(); runPostRequestHooks(configuration.getRootPanelId()); } // deferring to avoid flickering Scheduler.get().scheduleDeferred(new Command() { @Override public void execute() { if (!hasActiveRequest()) { hideLoadingIndicator(); // If on Liferay and session expiration management is in // use, extend session duration on each request. // Doing it here rather than before the request to improve // responsiveness. // Postponed until the end of the next request if other // requests still pending. extendLiferaySession(); } } }); } /** * This method is called after applying uidl change set to application. * * It will clean current and queued variable change sets. And send next * change set if it exists. */ private void checkForPendingVariableBursts() { cleanVariableBurst(pendingInvocations); if (pendingBursts.size() > 0) { for (Iterator> iterator = pendingBursts .iterator(); iterator.hasNext();) { cleanVariableBurst(iterator.next()); } ArrayList nextBurst = pendingBursts.get(0); pendingBursts.remove(0); buildAndSendVariableBurst(nextBurst, false); } } /** * Cleans given queue of variable changes of such changes that came from * components that do not exist anymore. * * @param variableBurst */ private void cleanVariableBurst(ArrayList variableBurst) { for (int i = 1; i < variableBurst.size(); i++) { String id = variableBurst.get(i).getConnectorId(); if (!getConnectorMap().hasConnector(id) && !getConnectorMap().isDragAndDropPaintable(id)) { // variable owner does not exist anymore variableBurst.remove(i); VConsole.log("Removed variable from removed component: " + id); } } } private void showLoadingIndicator() { // show initial throbber if (loadElement == null) { loadElement = DOM.createDiv(); DOM.setStyleAttribute(loadElement, "position", "absolute"); DOM.appendChild(rootConnector.getWidget().getElement(), loadElement); VConsole.log("inserting load indicator"); } DOM.setElementProperty(loadElement, "className", "v-loading-indicator"); DOM.setStyleAttribute(loadElement, "display", "block"); // Initialize other timers loadTimer2 = new Timer() { @Override public void run() { DOM.setElementProperty(loadElement, "className", "v-loading-indicator-delay"); } }; // Second one kicks in at 1500ms from request start loadTimer2.schedule(1200); loadTimer3 = new Timer() { @Override public void run() { DOM.setElementProperty(loadElement, "className", "v-loading-indicator-wait"); } }; // Third one kicks in at 5000ms from request start loadTimer3.schedule(4700); } private void hideLoadingIndicator() { if (loadTimer != null) { loadTimer.cancel(); loadTimer = null; } if (loadTimer2 != null) { loadTimer2.cancel(); loadTimer3.cancel(); loadTimer2 = null; loadTimer3 = null; } if (loadElement != null) { DOM.setStyleAttribute(loadElement, "display", "none"); } } /** * Checks if deferred commands are (potentially) still being executed as a * result of an update from the server. Returns true if a deferred command * might still be executing, false otherwise. This will not work correctly * if a deferred command is added in another deferred command. *

* Used by the native "client.isActive" function. *

* * @return true if deferred commands are (potentially) being executed, false * otherwise */ private boolean isExecutingDeferredCommands() { Scheduler s = Scheduler.get(); if (s instanceof VSchedulerImpl) { return ((VSchedulerImpl) s).hasWorkQueued(); } else { return false; } } /** * Determines whether or not the loading indicator is showing. * * @return true if the loading indicator is visible */ public boolean isLoadingIndicatorVisible() { if (loadElement == null) { return false; } if (loadElement.getStyle().getProperty("display").equals("none")) { return false; } return true; } private static native ValueMap parseJSONResponse(String jsonText) /*-{ try { return JSON.parse(jsonText); } catch (ignored) { return eval('(' + jsonText + ')'); } }-*/; private void handleReceivedJSONMessage(Date start, String jsonText, ValueMap json) { handleUIDLMessage(start, jsonText, json); } protected void handleUIDLMessage(final Date start, final String jsonText, final ValueMap json) { VConsole.log("Handling message from server"); // Handle redirect if (json.containsKey("redirect")) { String url = json.getValueMap("redirect").getString("url"); VConsole.log("redirecting to " + url); redirect(url); return; } final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); // Get security key if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { uidlSecurityKey = json .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); } VConsole.log(" * Handling resources from server"); if (json.containsKey("resources")) { ValueMap resources = json.getValueMap("resources"); JsArrayString keyArray = resources.getKeyArray(); int l = keyArray.length(); for (int i = 0; i < l; i++) { String key = keyArray.get(i); resourcesMap.put(key, resources.getAsString(key)); } } handleUIDLDuration.logDuration( " * Handling resources from server completed", 10); VConsole.log(" * Handling type inheritance map from server"); if (json.containsKey("typeInheritanceMap")) { configuration.addComponentInheritanceInfo(json .getValueMap("typeInheritanceMap")); } handleUIDLDuration.logDuration( " * Handling type inheritance map from server completed", 10); VConsole.log("Handling type mappings from server"); if (json.containsKey("typeMappings")) { configuration.addComponentMappings( json.getValueMap("typeMappings"), widgetSet); } VConsole.log("Handling resource dependencies"); if (json.containsKey("scriptDependencies")) { loadScriptDependencies(json.getJSStringArray("scriptDependencies")); } if (json.containsKey("styleDependencies")) { loadStyleDependencies(json.getJSStringArray("styleDependencies")); } handleUIDLDuration.logDuration( " * Handling type mappings from server completed", 10); /* * Hook for e.g. TestBench to get details about server peformance */ if (json.containsKey("timings")) { serverTimingInfo = json.getValueMap("timings"); } Command c = new Command() { @Override public void execute() { handleUIDLDuration.logDuration(" * Loading widgets completed", 10); MultiStepDuration updateDuration = new MultiStepDuration(); if (json.containsKey("locales")) { VConsole.log(" * Handling locales"); // Store locale data JsArray valueMapArray = json .getJSValueMapArray("locales"); LocaleService.addLocales(valueMapArray); } updateDuration.logDuration(" * Handling locales completed", 10); boolean repaintAll = false; ValueMap meta = null; if (json.containsKey("meta")) { VConsole.log(" * Handling meta information"); meta = json.getValueMap("meta"); if (meta.containsKey("repaintAll")) { repaintAll = true; rootConnector.getWidget().clear(); getConnectorMap().clear(); if (meta.containsKey("invalidLayouts")) { validatingLayouts = true; zeroWidthComponents = new HashSet(); zeroHeightComponents = new HashSet(); } } if (meta.containsKey("timedRedirect")) { final ValueMap timedRedirect = meta .getValueMap("timedRedirect"); redirectTimer = new Timer() { @Override public void run() { redirect(timedRedirect.getString("url")); } }; sessionExpirationInterval = timedRedirect .getInt("interval"); } } updateDuration.logDuration( " * Handling meta information completed", 10); if (redirectTimer != null) { redirectTimer.schedule(1000 * sessionExpirationInterval); } componentCaptionSizeChanges.clear(); int startProcessing = updateDuration.elapsedMillis(); // Ensure that all connectors that we are about to update exist createConnectorsIfNeeded(json); updateDuration.logDuration(" * Creating connectors completed", 10); // Update states, do not fire events Collection pendingStateChangeEvents = updateConnectorState(json); updateDuration.logDuration( " * Update of connector states completed", 10); // Update hierarchy, do not fire events Collection pendingHierarchyChangeEvents = updateConnectorHierarchy(json); updateDuration.logDuration( " * Update of connector hierarchy completed", 10); // Fire hierarchy change events sendHierarchyChangeEvents(pendingHierarchyChangeEvents); updateDuration.logDuration( " * Hierarchy state change event processing completed", 10); // Fire state change events. sendStateChangeEvents(pendingStateChangeEvents); updateDuration.logDuration( " * State change event processing completed", 10); // Update of legacy (UIDL) style connectors updateVaadin6StyleConnectors(json); updateDuration .logDuration( " * Vaadin 6 style connector updates (updateFromUidl) completed", 10); // Handle any RPC invocations done on the server side handleRpcInvocations(json); updateDuration.logDuration( " * Processing of RPC invocations completed", 10); if (json.containsKey("dd")) { // response contains data for drag and drop service VDragAndDropManager.get().handleServerResponse( json.getValueMap("dd")); } updateDuration .logDuration( " * Processing of drag and drop server response completed", 10); unregisterRemovedConnectors(); updateDuration.logDuration( " * Unregistering of removed components completed", 10); VConsole.log("handleUIDLMessage: " + (updateDuration.elapsedMillis() - startProcessing) + " ms"); LayoutManager layoutManager = getLayoutManager(); layoutManager.setEverythingNeedsMeasure(); layoutManager.layoutNow(); updateDuration .logDuration(" * Layout processing completed", 10); if (ApplicationConfiguration.isDebugMode()) { VConsole.log(" * Dumping state changes to the console"); VConsole.dirUIDL(json, ApplicationConnection.this); updateDuration .logDuration( " * Dumping state changes to the console completed", 10); } if (meta != null) { if (meta.containsKey("appError")) { ValueMap error = meta.getValueMap("appError"); String html = ""; if (error.containsKey("caption") && error.getString("caption") != null) { html += "

" + error.getAsString("caption") + "

"; } if (error.containsKey("message") && error.getString("message") != null) { html += "

" + error.getAsString("message") + "

"; } String url = null; if (error.containsKey("url")) { url = error.getString("url"); } if (html.length() != 0) { /* 45 min */ VNotification n = VNotification .createNotification(1000 * 60 * 45); n.addEventListener(new NotificationRedirect(url)); n.show(html, VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM); } else { redirect(url); } applicationRunning = false; } if (validatingLayouts) { VConsole.printLayoutProblems(meta, ApplicationConnection.this, zeroHeightComponents, zeroWidthComponents); zeroHeightComponents = null; zeroWidthComponents = null; validatingLayouts = false; } } updateDuration.logDuration(" * Error handling completed", 10); // TODO build profiling for widget impl loading time lastProcessingTime = (int) ((new Date().getTime()) - start .getTime()); totalProcessingTime += lastProcessingTime; VConsole.log(" Processing time was " + String.valueOf(lastProcessingTime) + "ms for " + jsonText.length() + " characters of JSON"); VConsole.log("Referenced paintables: " + connectorMap.size()); endRequest(); } /** * Sends the state change events created while updating the state * information. * * This must be called after hierarchy change listeners have been * called. At least caption updates for the parent are strange if * fired from state change listeners and thus calls the parent * BEFORE the parent is aware of the child (through a * ConnectorHierarchyChangedEvent) * * @param pendingStateChangeEvents * The events to send */ private void sendStateChangeEvents( Collection pendingStateChangeEvents) { VConsole.log(" * Sending state change events"); for (StateChangeEvent sce : pendingStateChangeEvents) { try { sce.getConnector().fireEvent(sce); } catch (final Throwable e) { VConsole.error(e); } } } private void unregisterRemovedConnectors() { int unregistered = 0; List currentConnectors = new ArrayList( connectorMap.getConnectors()); for (ServerConnector c : currentConnectors) { if (c.getParent() != null) { if (!c.getParent().getChildren().contains(c)) { VConsole.error("ERROR: Connector is connected to a parent but the parent does not contain the connector"); } } else if ((c instanceof RootConnector && c == getRootConnector())) { // RootConnector for this connection, leave as-is } else if (c instanceof WindowConnector && getRootConnector().hasSubWindow( (WindowConnector) c)) { // Sub window attached to this RootConnector, leave // as-is } else { // The connector has been detached from the // hierarchy, unregister it and any possible // children. The RootConnector should never be // unregistered even though it has no parent. connectorMap.unregisterConnector(c); unregistered++; } } VConsole.log("* Unregistered " + unregistered + " connectors"); } private void createConnectorsIfNeeded(ValueMap json) { VConsole.log(" * Creating connectors (if needed)"); if (!json.containsKey("types")) { return; } ValueMap types = json.getValueMap("types"); JsArrayString keyArray = types.getKeyArray(); for (int i = 0; i < keyArray.length(); i++) { try { String connectorId = keyArray.get(i); int connectorType = Integer.parseInt(types .getString((connectorId))); ServerConnector connector = connectorMap .getConnector(connectorId); if (connector != null) { continue; } Class connectorClass = configuration .getConnectorClassByEncodedTag(connectorType); // Connector does not exist so we must create it if (connectorClass != RootConnector.class) { // create, initialize and register the paintable getConnector(connectorId, connectorType); } else { // First RootConnector update. Before this the // RootConnector has been created but not // initialized as the connector id has not been // known connectorMap.registerConnector(connectorId, rootConnector); rootConnector.doInit(connectorId, ApplicationConnection.this); } } catch (final Throwable e) { VConsole.error(e); } } } private void updateVaadin6StyleConnectors(ValueMap json) { JsArray changes = json.getJSValueMapArray("changes"); int length = changes.length(); VConsole.log(" * Passing UIDL to Vaadin 6 style connectors"); // update paintables for (int i = 0; i < length; i++) { try { final UIDL change = changes.get(i).cast(); final UIDL uidl = change.getChildUIDL(0); String connectorId = uidl.getId(); final ComponentConnector legacyConnector = (ComponentConnector) connectorMap .getConnector(connectorId); if (legacyConnector instanceof Paintable) { ((Paintable) legacyConnector).updateFromUIDL(uidl, ApplicationConnection.this); } else if (legacyConnector == null) { VConsole.error("Received update for " + uidl.getTag() + ", but there is no such paintable (" + connectorId + ") rendered."); } else { VConsole.error("Server sent Vaadin 6 style updates for " + Util.getConnectorString(legacyConnector) + " but this is not a Vaadin 6 Paintable"); } } catch (final Throwable e) { VConsole.error(e); } } } private void sendHierarchyChangeEvents( Collection pendingHierarchyChangeEvents) { if (pendingHierarchyChangeEvents.isEmpty()) { return; } VConsole.log(" * Sending hierarchy change events"); for (ConnectorHierarchyChangeEvent event : pendingHierarchyChangeEvents) { try { event.getConnector().fireEvent(event); } catch (final Throwable e) { VConsole.error(e); } } } private Collection updateConnectorState( ValueMap json) { ArrayList events = new ArrayList(); VConsole.log(" * Updating connector states"); if (!json.containsKey("state")) { return events; } // set states for all paintables mentioned in "state" ValueMap states = json.getValueMap("state"); JsArrayString keyArray = states.getKeyArray(); for (int i = 0; i < keyArray.length(); i++) { try { String connectorId = keyArray.get(i); ServerConnector connector = connectorMap .getConnector(connectorId); if (null != connector) { JSONObject stateJson = new JSONObject( states.getJavaScriptObject(connectorId)); if (connector instanceof HasJavaScriptConnectorHelper) { ((HasJavaScriptConnectorHelper) connector) .getJavascriptConnectorHelper() .setNativeState( stateJson.getJavaScriptObject()); } SharedState state = connector.getState(); JsonDecoder.decodeValue(new Type(state.getClass() .getName(), null), stateJson, state, ApplicationConnection.this); StateChangeEvent event = GWT .create(StateChangeEvent.class); event.setConnector(connector); events.add(event); } } catch (final Throwable e) { VConsole.error(e); } } return events; } /** * Updates the connector hierarchy and returns a list of events that * should be fired after update of the hierarchy and the state is * done. * * @param json * The JSON containing the hierarchy information * @return A collection of events that should be fired when update * of hierarchy and state is complete */ private Collection updateConnectorHierarchy( ValueMap json) { List events = new LinkedList(); VConsole.log(" * Updating connector hierarchy"); if (!json.containsKey("hierarchy")) { return events; } ValueMap hierarchies = json.getValueMap("hierarchy"); JsArrayString hierarchyKeys = hierarchies.getKeyArray(); for (int i = 0; i < hierarchyKeys.length(); i++) { try { String connectorId = hierarchyKeys.get(i); ServerConnector parentConnector = connectorMap .getConnector(connectorId); JsArrayString childConnectorIds = hierarchies .getJSStringArray(connectorId); int childConnectorSize = childConnectorIds.length(); List newChildren = new ArrayList(); List newComponents = new ArrayList(); for (int connectorIndex = 0; connectorIndex < childConnectorSize; connectorIndex++) { String childConnectorId = childConnectorIds .get(connectorIndex); ServerConnector childConnector = connectorMap .getConnector(childConnectorId); if (childConnector == null) { VConsole.error("Hierarchy claims that " + childConnectorId + " is a child for " + connectorId + " (" + parentConnector.getClass().getName() + ") but no connector with id " + childConnectorId + " has been registered"); continue; } newChildren.add(childConnector); if (childConnector instanceof ComponentConnector) { newComponents .add((ComponentConnector) childConnector); } else if (!(childConnector instanceof AbstractExtensionConnector)) { throw new IllegalStateException( Util.getConnectorString(childConnector) + " is not a ComponentConnector nor an AbstractExtensionConnector"); } if (childConnector.getParent() != parentConnector) { // Avoid extra calls to setParent childConnector.setParent(parentConnector); } } // TODO This check should be done on the server side in // the future so the hierarchy update is only sent when // something actually has changed List oldChildren = parentConnector .getChildren(); boolean actuallyChanged = !Util.collectionsEquals( oldChildren, newChildren); if (!actuallyChanged) { continue; } if (parentConnector instanceof ComponentContainerConnector) { ComponentContainerConnector ccc = (ComponentContainerConnector) parentConnector; List oldComponents = ccc .getChildComponents(); if (!Util.collectionsEquals(oldComponents, newComponents)) { // Fire change event if the hierarchy has // changed ConnectorHierarchyChangeEvent event = GWT .create(ConnectorHierarchyChangeEvent.class); event.setOldChildren(oldComponents); event.setConnector(parentConnector); ccc.setChildComponents(newComponents); events.add(event); } } else if (!newComponents.isEmpty()) { VConsole.error("Hierachy claims " + Util.getConnectorString(parentConnector) + " has component children even though it isn't a ComponentContainerConnector"); } parentConnector.setChildren(newChildren); // Remove parent for children that are no longer // attached to this (avoid updating children if they // have already been assigned to a new parent) for (ServerConnector oldChild : oldChildren) { if (oldChild.getParent() != parentConnector) { continue; } // TODO This could probably be optimized if (!newChildren.contains(oldChild)) { oldChild.setParent(null); } } } catch (final Throwable e) { VConsole.error(e); } } return events; } private void handleRpcInvocations(ValueMap json) { if (json.containsKey("rpc")) { VConsole.log(" * Performing server to client RPC calls"); JSONArray rpcCalls = new JSONArray( json.getJavaScriptObject("rpc")); int rpcLength = rpcCalls.size(); for (int i = 0; i < rpcLength; i++) { try { JSONArray rpcCall = (JSONArray) rpcCalls.get(i); rpcManager.parseAndApplyInvocation(rpcCall, ApplicationConnection.this); } catch (final Throwable e) { VConsole.error(e); } } } } }; ApplicationConfiguration.runWhenDependenciesLoaded(c); } private void loadStyleDependencies(JsArrayString dependencies) { // Assuming no reason to interpret in a defined order ResourceLoadListener resourceLoadListener = new ResourceLoadListener() { @Override public void onLoad(ResourceLoadEvent event) { ApplicationConfiguration.endDependencyLoading(); } @Override public void onError(ResourceLoadEvent event) { VConsole.error(event.getResourceUrl() + " could not be loaded, or the load detection failed because the stylesheet is empty."); // The show must go on onLoad(event); } }; ResourceLoader loader = ResourceLoader.get(); for (int i = 0; i < dependencies.length(); i++) { String url = translateVaadinUri(dependencies.get(i)); ApplicationConfiguration.startDependencyLoading(); loader.loadStylesheet(url, resourceLoadListener); } } private void loadScriptDependencies(final JsArrayString dependencies) { if (dependencies.length() == 0) { return; } // Listener that loads the next when one is completed ResourceLoadListener resourceLoadListener = new ResourceLoadListener() { @Override public void onLoad(ResourceLoadEvent event) { if (dependencies.length() != 0) { String url = translateVaadinUri(dependencies.shift()); ApplicationConfiguration.startDependencyLoading(); // Load next in chain (hopefully already preloaded) event.getResourceLoader().loadScript(url, this); } // Call start for next before calling end for current ApplicationConfiguration.endDependencyLoading(); } @Override public void onError(ResourceLoadEvent event) { VConsole.error(event.getResourceUrl() + " could not be loaded."); // The show must go on onLoad(event); } }; ResourceLoader loader = ResourceLoader.get(); // Start chain by loading first String url = translateVaadinUri(dependencies.shift()); ApplicationConfiguration.startDependencyLoading(); loader.loadScript(url, resourceLoadListener); // Preload all remaining for (int i = 0; i < dependencies.length(); i++) { String preloadUrl = translateVaadinUri(dependencies.get(i)); loader.preloadResource(preloadUrl, null); } } // Redirect browser, null reloads current page private static native void redirect(String url) /*-{ if (url) { $wnd.location = url; } else { $wnd.location.reload(false); } }-*/; private void addVariableToQueue(String connectorId, String variableName, Object value, boolean immediate) { // note that type is now deduced from value // TODO could eliminate invocations of same shared variable setter addMethodInvocationToQueue(new MethodInvocation(connectorId, ApplicationConstants.UPDATE_VARIABLE_INTERFACE, ApplicationConstants.UPDATE_VARIABLE_METHOD, new Object[] { variableName, new UidlValue(value) }), immediate); } /** * Adds an explicit RPC method invocation to the send queue. * * @since 7.0 * * @param invocation * RPC method invocation * @param immediate * true to trigger sending within a short time window (possibly * combining subsequent calls to a single request), false to let * the framework delay sending of RPC calls and variable changes * until the next immediate change */ public void addMethodInvocationToQueue(MethodInvocation invocation, boolean immediate) { pendingInvocations.add(invocation); if (immediate) { sendPendingVariableChanges(); } } /** * This method sends currently queued variable changes to server. It is * called when immediate variable update must happen. * * To ensure correct order for variable changes (due servers multithreading * or network), we always wait for active request to be handler before * sending a new one. If there is an active request, we will put varible * "burst" to queue that will be purged after current request is handled. * */ public void sendPendingVariableChanges() { if (!deferedSendPending) { deferedSendPending = true; Scheduler.get().scheduleDeferred(sendPendingCommand); } } private final ScheduledCommand sendPendingCommand = new ScheduledCommand() { @Override public void execute() { deferedSendPending = false; doSendPendingVariableChanges(); } }; private boolean deferedSendPending = false; @SuppressWarnings("unchecked") private void doSendPendingVariableChanges() { if (applicationRunning) { if (hasActiveRequest()) { // skip empty queues if there are pending bursts to be sent if (pendingInvocations.size() > 0 || pendingBursts.size() == 0) { pendingBursts.add(pendingInvocations); pendingInvocations = new ArrayList(); } } else { buildAndSendVariableBurst(pendingInvocations, false); } } } /** * Build the variable burst and send it to server. * * When sync is forced, we also force sending of all pending variable-bursts * at the same time. This is ok as we can assume that DOM will never be * updated after this. * * @param pendingInvocations * List of RPC method invocations to send * @param forceSync * Should we use synchronous request? */ private void buildAndSendVariableBurst( ArrayList pendingInvocations, boolean forceSync) { final StringBuffer req = new StringBuffer(); while (!pendingInvocations.isEmpty()) { if (ApplicationConfiguration.isDebugMode()) { Util.logVariableBurst(this, pendingInvocations); } JSONArray reqJson = new JSONArray(); for (MethodInvocation invocation : pendingInvocations) { JSONArray invocationJson = new JSONArray(); invocationJson.set(0, new JSONString(invocation.getConnectorId())); invocationJson.set(1, new JSONString(invocation.getInterfaceName())); invocationJson.set(2, new JSONString(invocation.getMethodName())); JSONArray paramJson = new JSONArray(); boolean restrictToInternalTypes = isLegacyVariableChange(invocation); for (int i = 0; i < invocation.getParameters().length; ++i) { // TODO non-static encoder? type registration? paramJson.set(i, JsonEncoder.encode( invocation.getParameters()[i], restrictToInternalTypes, this)); } invocationJson.set(3, paramJson); reqJson.set(reqJson.size(), invocationJson); } // escape burst separators (if any) req.append(escapeBurstContents(reqJson.toString())); pendingInvocations.clear(); // Append all the bursts to this synchronous request if (forceSync && !pendingBursts.isEmpty()) { pendingInvocations = pendingBursts.get(0); pendingBursts.remove(0); req.append(VAR_BURST_SEPARATOR); } } // Include the browser detail parameters if they aren't already sent String extraParams; if (!getConfiguration().isBrowserDetailsSent()) { extraParams = getNativeBrowserDetailsParameters(getConfiguration() .getRootPanelId()); getConfiguration().setBrowserDetailsSent(); } else { extraParams = ""; } if (!getConfiguration().isWidgetsetVersionSent()) { if (!extraParams.isEmpty()) { extraParams += "&"; } String widgetsetVersion = Version.getFullVersion(); extraParams += "wsver=" + widgetsetVersion; getConfiguration().setWidgetsetVersionSent(); } makeUidlRequest(req.toString(), extraParams, forceSync); } private boolean isLegacyVariableChange(MethodInvocation invocation) { return ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation .getInterfaceName()) && ApplicationConstants.UPDATE_VARIABLE_METHOD .equals(invocation.getMethodName()); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, ServerConnector newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, String newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, int newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, long newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, float newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, double newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param newValue * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, boolean newValue, boolean immediate) { addVariableToQueue(paintableId, variableName, newValue, immediate); } /** * Sends a new value for the given paintables given variable to the server. *

* The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. *

* * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param map * the new values to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, Map map, boolean immediate) { addVariableToQueue(paintableId, variableName, map, immediate); } /** * Sends a new value for the given paintables given variable to the server. * * The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update. * * A null array is sent as an empty array. * * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param values * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, String[] values, boolean immediate) { addVariableToQueue(paintableId, variableName, values, immediate); } /** * Sends a new value for the given paintables given variable to the server. * * The update is actually queued to be sent at a suitable time. If immediate * is true, the update is sent as soon as possible. If immediate is false, * the update will be sent along with the next immediate update.

* * A null array is sent as an empty array. * * * @param paintableId * the id of the paintable that owns the variable * @param variableName * the name of the variable * @param values * the new value to be sent * @param immediate * true if the update is to be sent as soon as possible */ public void updateVariable(String paintableId, String variableName, Object[] values, boolean immediate) { addVariableToQueue(paintableId, variableName, values, immediate); } /** * Encode burst separator characters in a String for transport over the * network. This protects from separator injection attacks. * * @param value * to encode * @return encoded value */ protected String escapeBurstContents(String value) { final StringBuilder result = new StringBuilder(); for (int i = 0; i < value.length(); ++i) { char character = value.charAt(i); switch (character) { case VAR_ESCAPE_CHARACTER: // fall-through - escape character is duplicated case VAR_BURST_SEPARATOR: result.append(VAR_ESCAPE_CHARACTER); // encode as letters for easier reading result.append(((char) (character + 0x30))); break; default: // the char is not a special one - add it to the result as is result.append(character); break; } } return result.toString(); } private boolean runningLayout = false; /** * Causes a re-calculation/re-layout of all paintables in a container. * * @param container */ public void runDescendentsLayout(HasWidgets container) { if (runningLayout) { return; } runningLayout = true; internalRunDescendentsLayout(container); runningLayout = false; } /** * This will cause re-layouting of all components. Mainly used for * development. Published to JavaScript. */ public void forceLayout() { Duration duration = new Duration(); layoutManager.forceLayout(); VConsole.log("forceLayout in " + duration.elapsedMillis() + " ms"); } private void internalRunDescendentsLayout(HasWidgets container) { // getConsole().log( // "runDescendentsLayout(" + Util.getSimpleName(container) + ")"); final Iterator childWidgets = container.iterator(); while (childWidgets.hasNext()) { final Widget child = childWidgets.next(); if (getConnectorMap().isConnector(child)) { if (handleComponentRelativeSize(child)) { /* * Only need to propagate event if "child" has a relative * size */ if (child instanceof ContainerResizedListener) { ((ContainerResizedListener) child).iLayout(); } if (child instanceof HasWidgets) { final HasWidgets childContainer = (HasWidgets) child; internalRunDescendentsLayout(childContainer); } } } else if (child instanceof HasWidgets) { // propagate over non Paintable HasWidgets internalRunDescendentsLayout((HasWidgets) child); } } } /** * Converts relative sizes into pixel sizes. * * @param child * @return true if the child has a relative size */ private boolean handleComponentRelativeSize(ComponentConnector paintable) { return false; } /** * Converts relative sizes into pixel sizes. * * @param child * @return true if the child has a relative size */ public boolean handleComponentRelativeSize(Widget widget) { return handleComponentRelativeSize(connectorMap.getConnector(widget)); } @Deprecated public ComponentConnector getPaintable(UIDL uidl) { // Non-component connectors shouldn't be painted from legacy connectors return (ComponentConnector) getConnector(uidl.getId(), Integer.parseInt(uidl.getTag())); } /** * Get either an existing ComponentConnector or create a new * ComponentConnector with the given type and id. * * If a ComponentConnector with the given id already exists, returns it. * Otherwise creates and registers a new ComponentConnector of the given * type. * * @param connectorId * Id of the paintable * @param connectorType * Type of the connector, as passed from the server side * * @return Either an existing ComponentConnector or a new ComponentConnector * of the given type */ public ServerConnector getConnector(String connectorId, int connectorType) { if (!connectorMap.hasConnector(connectorId)) { return createAndRegisterConnector(connectorId, connectorType); } return connectorMap.getConnector(connectorId); } /** * Creates a new ServerConnector with the given type and id. * * Creates and registers a new ServerConnector of the given type. Should * never be called with the connector id of an existing connector. * * @param connectorId * Id of the new connector * @param connectorType * Type of the connector, as passed from the server side * * @return A new ServerConnector of the given type */ private ServerConnector createAndRegisterConnector(String connectorId, int connectorType) { // Create and register a new connector with the given type ServerConnector p = widgetSet.createConnector(connectorType, configuration); connectorMap.registerConnector(connectorId, p); p.doInit(connectorId, this); return p; } /** * Gets a recource that has been pre-loaded via UIDL, such as custom * layouts. * * @param name * identifier of the resource to get * @return the resource */ public String getResource(String name) { return resourcesMap.get(name); } /** * Singleton method to get instance of app's context menu. * * @return VContextMenu object */ public VContextMenu getContextMenu() { if (contextMenu == null) { contextMenu = new VContextMenu(); DOM.setElementProperty(contextMenu.getElement(), "id", "PID_VAADIN_CM"); } return contextMenu; } /** * Translates custom protocols in UIDL URI's to be recognizable by browser. * All uri's from UIDL should be routed via this method before giving them * to browser due URI's in UIDL may contain custom protocols like theme://. * * @param uidlUri * Vaadin URI from uidl * @return translated URI ready for browser */ public String translateVaadinUri(String uidlUri) { if (uidlUri == null) { return null; } if (uidlUri.startsWith("theme://")) { final String themeUri = configuration.getThemeUri(); if (themeUri == null) { VConsole.error("Theme not set: ThemeResource will not be found. (" + uidlUri + ")"); } uidlUri = themeUri + uidlUri.substring(7); } if (uidlUri.startsWith(ApplicationConstants.CONNECTOR_PROTOCOL_PREFIX)) { // getAppUri *should* always end with / // substring *should* always start with / (connector:///foo.bar // without connector://) uidlUri = ApplicationConstants.APP_PROTOCOL_PREFIX + ApplicationConstants.CONNECTOR_RESOURCE_PREFIX + uidlUri .substring(ApplicationConstants.CONNECTOR_PROTOCOL_PREFIX .length()); // Let translation of app:// urls take care of the rest } if (uidlUri.startsWith(ApplicationConstants.APP_PROTOCOL_PREFIX)) { String relativeUrl = uidlUri .substring(ApplicationConstants.APP_PROTOCOL_PREFIX .length()); if (getConfiguration().usePortletURLs()) { // Should put path in v-resourcePath parameter and append query // params to base portlet url String[] parts = relativeUrl.split("\\?", 2); String path = parts[0]; String url = getConfiguration().getPortletResourceUrl(); // If there's a "?" followed by something, append it as a query // string to the base URL if (parts.length > 1) { String appUrlParams = parts[1]; url = addGetParameters(url, appUrlParams); } if (!path.startsWith("/")) { path = '/' + path; } String pathParam = ApplicationConstants.V_RESOURCE_PATH + "=" + URL.encodeQueryString(path); url = addGetParameters(url, pathParam); uidlUri = url; } else { uidlUri = getAppUri() + relativeUrl; } } return uidlUri; } /** * Gets the URI for the current theme. Can be used to reference theme * resources. * * @return URI to the current theme */ public String getThemeUri() { return configuration.getThemeUri(); } /** * Listens for Notification hide event, and redirects. Used for system * messages, such as session expired. * */ private class NotificationRedirect implements VNotification.EventListener { String url; NotificationRedirect(String url) { this.url = url; } @Override public void notificationHidden(HideEvent event) { redirect(url); } } /* Extended title handling */ private final VTooltip tooltip = new VTooltip(this); private ConnectorMap connectorMap = GWT.create(ConnectorMap.class); protected String getUidlSecurityKey() { return uidlSecurityKey; } /** * Use to notify that the given component's caption has changed; layouts may * have to be recalculated. * * @param component * the Paintable whose caption has changed */ public void captionSizeUpdated(Widget widget) { componentCaptionSizeChanges.add(widget); } /** * Gets the main view * * @return the main view */ public RootConnector getRootConnector() { return rootConnector; } /** * Gets the {@link ApplicationConfiguration} for the current application. * * @see ApplicationConfiguration * @return the configuration for this application */ public ApplicationConfiguration getConfiguration() { return configuration; } /** * Checks if there is a registered server side listener for the event. The * list of events which has server side listeners is updated automatically * before the component is updated so the value is correct if called from * updatedFromUIDL. * * @param paintable * The connector to register event listeners for * @param eventIdentifier * The identifier for the event * @return true if at least one listener has been registered on server side * for the event identified by eventIdentifier. * @deprecated Use {@link ComponentState#hasEventListener(String)} instead */ @Deprecated public boolean hasEventListeners(ComponentConnector paintable, String eventIdentifier) { return paintable.hasEventListener(eventIdentifier); } /** * Adds the get parameters to the uri and returns the new uri that contains * the parameters. * * @param uri * The uri to which the parameters should be added. * @param extraParams * One or more parameters in the format "a=b" or "c=d&e=f". An * empty string is allowed but will not modify the url. * @return The modified URI with the get parameters in extraParams added. */ public static String addGetParameters(String uri, String extraParams) { if (extraParams == null || extraParams.length() == 0) { return uri; } // RFC 3986: The query component is indicated by the first question // mark ("?") character and terminated by a number sign ("#") character // or by the end of the URI. String fragment = null; int hashPosition = uri.indexOf('#'); if (hashPosition != -1) { // Fragment including "#" fragment = uri.substring(hashPosition); // The full uri before the fragment uri = uri.substring(0, hashPosition); } if (uri.contains("?")) { uri += "&"; } else { uri += "?"; } uri += extraParams; if (fragment != null) { uri += fragment; } return uri; } ConnectorMap getConnectorMap() { return connectorMap; } @Deprecated public void unregisterPaintable(ServerConnector p) { System.out.println("unregisterPaintable (unnecessarily) called for " + Util.getConnectorString(p)); // connectorMap.unregisterConnector(p); } /** * Get VTooltip instance related to application connection * * @return VTooltip instance */ public VTooltip getVTooltip() { return tooltip; } /** * Method provided for backwards compatibility. Duties previously done by * this method is now handled by the state change event handler in * AbstractComponentConnector. The only function this method has is to * return true if the UIDL is a "cached" update. * * @param component * @param uidl * @param manageCaption * @return */ @Deprecated public boolean updateComponent(Widget component, UIDL uidl, boolean manageCaption) { ComponentConnector connector = getConnectorMap() .getConnector(component); if (!AbstractComponentConnector.isRealUpdate(uidl)) { return true; } if (!manageCaption) { VConsole.error(Util.getConnectorString(connector) + " called updateComponent with manageCaption=false. The parameter was ignored - override delegateCaption() to return false instead. It is however not recommended to use caption this way at all."); } return false; } @Deprecated public boolean hasEventListeners(Widget widget, String eventIdentifier) { return hasEventListeners(getConnectorMap().getConnector(widget), eventIdentifier); } LayoutManager getLayoutManager() { return layoutManager; } public SerializerMap getSerializerMap() { return serializerMap; } }