/*
* 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.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
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.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.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.Event;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
import com.vaadin.terminal.gwt.client.RenderInformation.Size;
import com.vaadin.terminal.gwt.client.ui.Field;
import com.vaadin.terminal.gwt.client.ui.VContextMenu;
import com.vaadin.terminal.gwt.client.ui.VNotification;
import com.vaadin.terminal.gwt.client.ui.VNotification.HideEvent;
import com.vaadin.terminal.gwt.client.ui.VView;
import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
import com.vaadin.terminal.gwt.server.AbstractCommunicationManager;
/**
* This is the client side communication "engine", managing client-server
* communication with its server side counterpart
* {@link AbstractCommunicationManager}.
*
* Client-side widgets receive updates from the corresponding server-side
* components as calls to
* {@link Paintable#updateFromUIDL(UIDL, ApplicationConnection)} (not to be
* confused with the server side interface {@link com.vaadin.terminal.Paintable}
* ). Any client-side changes (typically resulting from user actions) are sent
* back to the server as variable changes (see {@link #updateVariable()}).
*
* TODO document better
*
* Entry point classes (widgetsets) define onModuleLoad().
*/
public class ApplicationConnection {
// This indicates the whole page is generated by us (not embedded)
public static final String GENERATED_BODY_CLASSNAME = "v-generated-body";
private static final String MODIFIED_CLASSNAME = "v-modified";
public static final String DISABLED_CLASSNAME = "v-disabled";
private static final String REQUIRED_CLASSNAME_EXT = "-required";
private static final String ERROR_CLASSNAME_EXT = "-error";
public static final char VAR_RECORD_SEPARATOR = '\u001e';
public static final char VAR_FIELD_SEPARATOR = '\u001f';
public static final char VAR_BURST_SEPARATOR = '\u001d';
public static final char VAR_ARRAYITEM_SEPARATOR = '\u001c';
public static final char VAR_ESCAPE_CHARACTER = '\u001b';
public static final String UIDL_SECURITY_TOKEN_ID = "Vaadin-Security-Key";
/**
* @deprecated use UIDL_SECURITY_TOKEN_ID instead
*/
@Deprecated
public static final String UIDL_SECURITY_HEADER = UIDL_SECURITY_TOKEN_ID;
public static final String PARAM_UNLOADBURST = "onunloadburst";
public static final String ATTRIBUTE_DESCRIPTION = "description";
public static final String ATTRIBUTE_ERROR = "error";
/**
* 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 HashMapvaadin.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.
* "); 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() { 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(pendingVariables); if (pendingVariableBursts.size() > 0) { for (Iterator* 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) { // Handle redirect if (json.containsKey("redirect")) { String url = json.getValueMap("redirect").getString("url"); VConsole.log("redirecting to " + url); redirect(url); return; } // Get security key if (json.containsKey(UIDL_SECURITY_TOKEN_ID)) { uidlSecurityKey = json.getString(UIDL_SECURITY_TOKEN_ID); } 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)); } } if (json.containsKey("typeMappings")) { configuration.addComponentMappings( json.getValueMap("typeMappings"), widgetSet); } /* * Hook for e.g. TestBench to get details about server peformance */ if (json.containsKey("timings")) { serverTimingInfo = json.getValueMap("timings"); } Command c = new Command() { public void execute() { VConsole.dirUIDL(json, configuration); if (json.containsKey("locales")) { // Store locale data JsArray" + 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; } } if (repaintAll) { /* * idToPaintableDetail is already cleanded at the start of * the changeset handling, bypass cleanup. */ unregistryBag.clear(); } else { purgeUnregistryBag(); } // TODO build profiling for widget impl loading time lastProcessingTime = (int) ((new Date().getTime()) - start .getTime()); totalProcessingTime += lastProcessingTime; if (bootstrapTime == 0) { bootstrapTime = calculateBootstrapTime(); } VConsole.log(" Processing time was " + String.valueOf(lastProcessingTime) + "ms for " + jsonText.length() + " characters of JSON"); VConsole.log("Referenced paintables: " + idToPaintableDetail.size()); endRequest(); } }; ApplicationConfiguration.runWhenWidgetsLoaded(c); } /** * This method assures that all pending variable changes are sent to server. * Method uses synchronized xmlhttprequest and does not return before the * changes are sent. No UIDL updates are processed and thus UI is left in * inconsistent state. This method should be called only when closing * windows - normally sendPendingVariableChanges() should be used. */ public void sendPendingVariableChangesSync() { if (applicationRunning) { pendingVariableBursts.add(pendingVariables); ArrayList* The paintableId is used in the UIDL to identify a specific widget * instance, effectively linking the widget with it's server side Component. *
* * @param paintable * the paintable who's id is needed * @return the id for the given paintable */ public String getPid(Paintable paintable) { return getPid(((Widget) paintable).getElement()); } /** * Gets the paintableId using a DOM element - the element should be the main * element for a paintable otherwise no id will be found. Use * {@link #getPid(Paintable)} instead whenever possible. * * @see #getPid(Paintable) * @param el * element of the paintable whose pid is desired * @return the pid of the element's paintable, if it's a paintable */ public native String getPid(Element el) /*-{ return el.tkPid; }-*/; /** * Gets the main element for the paintable with the given id. The revers of * {@link #getPid(Element)}. * * @param pid * the pid of the widget whose element is desired * @return the element for the paintable corresponding to the pid */ public Element getElementByPid(String pid) { return ((Widget) getPaintable(pid)).getElement(); } /** * Unregisters the given paintable; always use after removing a paintable. * This method does not remove the paintable from the DOM, but marks the * paintable so that ApplicationConnection may clean up its references to * it. Removing the widget from DOM is component containers responsibility. * * @param p * the paintable to remove */ public void unregisterPaintable(Paintable p) { // add to unregistry que if (p == null) { VConsole.error("WARN: Trying to unregister null paintable"); return; } String id = getPid(p); if (id == null) { /* * Uncomment the following to debug unregistring components. No * paintables with null id should end here. At least one exception * is our VScrollTableRow, that is hacked to fake it self as a * Paintable to build support for sizing easier. */ // if (!(p instanceof VScrollTableRow)) { // VConsole.log("Trying to unregister Paintable not created by Application Connection."); // } if (p instanceof HasWidgets) { unregisterChildPaintables((HasWidgets) p); } } else { unregistryBag.add(id); if (p instanceof HasWidgets) { unregisterChildPaintables((HasWidgets) p); } } } private void purgeUnregistryBag() { for (String id : unregistryBag) { ComponentDetail componentDetail = idToPaintableDetail.get(id); if (componentDetail == null) { /* * this should never happen, but it does :-( See e.g. * com.vaadin.tests.components.accordion.RemoveTabs (with test * script) */ VConsole.error("ApplicationConnetion tried to unregister component (id=" + id + ") that is never registered (or already unregistered)"); continue; } // check if can be cleaned Widget component = (Widget) componentDetail.getComponent(); if (!component.isAttached()) { // clean reference from ac to paintable idToPaintableDetail.remove(id); } /* * else NOP : same component has been reattached to another parent * or replaced by another component implementation. */ } unregistryBag.clear(); } /** * Unregisters a paintable and all it's child paintables recursively. Use * when after removing a paintable that contains other paintables. Does not * unregister the given container itself. Does not actually remove the * paintable from the DOM. * * @see #unregisterPaintable(Paintable) * @param container */ public void unregisterChildPaintables(HasWidgets container) { final Iterator* 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, Paintable newValue, boolean immediate) { String pid = (newValue != null) ? getPid(newValue) : null; addVariableToQueue(paintableId, variableName, pid, immediate, 'p'); } /** * 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, escapeVariableValue(newValue), immediate, 's'); } /** * 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, 'i'); } /** * 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, 'l'); } /** * 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, 'f'); } /** * 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, 'd'); } /** * 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 ? "true" : "false", immediate, 'b'); } /** * 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, Map* The implementation of a component depends on many properties, including * styles, component features, etc. Sometimes the user changes those * properties after the component has been created. Calling this method in * the beginning of your updateFromUIDL -method automatically replaces your * component with more appropriate if the requested implementation changes. *
* ** Component can delegate management of caption, icon, error messages and * description to parent layout. This is optional an should be decided by * component author *
* ** Component must also pipe events to * {@link #handleTooltipEvent(Event, Paintable, Object)} method. *
* This method can also be used to deregister tooltips by using null as * tooltip * * @param paintable * Paintable "owning" this tooltip * @param key * key assosiated with given tooltip. Can be any object. For * example a related dom element. Same key must be given for * {@link #handleTooltipEvent(Event, Paintable, Object)} method. * * @param tooltip * the TooltipInfo object containing details shown in tooltip, * null if deregistering tooltip */ public void registerTooltip(Paintable paintable, Object key, TooltipInfo tooltip) { ComponentDetail componentDetail = idToPaintableDetail .get(getPid(paintable)); componentDetail.putAdditionalTooltip(key, tooltip); } /** * 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 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. */ public boolean hasEventListeners(Paintable paintable, String eventIdentifier) { return idToPaintableDetail.get(getPid(paintable)).hasEventListeners( 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; } /** * Removes the cached size info for the paintable. This is for really edge * cases only and should normally not be called by anyone. * * @param p * The paintable whose cached size info should be discarded */ public void resetCachedSize(Paintable p) { idToPaintableDetail.get(getPid(p)).setOffsetSize(null); } }