
com.vaadin.terminal.gwt.client.ApplicationConnection Maven / Gradle / Ivy
Show all versions of vaadin Show documentation
/*
* 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.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.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.MethodInvocation;
import com.vaadin.terminal.gwt.client.communication.RpcManager;
import com.vaadin.terminal.gwt.client.communication.SerializerMap;
import com.vaadin.terminal.gwt.client.communication.SharedState;
import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
import com.vaadin.terminal.gwt.client.communication.Type;
import com.vaadin.terminal.gwt.client.communication.UidlValue;
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;
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 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 {
private static final String CONNECTOR_PROTOCOL_PREFIX = "connector://";
public static final String CONNECTOR_RESOURCE_PREFIX = "APP/CONNECTOR";
// This indicates the whole page is generated by us (not embedded)
public static final String GENERATED_BODY_CLASSNAME = "v-generated-body";
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 String UPDATE_VARIABLE_INTERFACE = "v";
public static final String UPDATE_VARIABLE_METHOD = "v";
public static final char VAR_BURST_SEPARATOR = '\u001d';
public static final char VAR_ESCAPE_CHARACTER = '\u001b';
public static final String UIDL_SECURITY_TOKEN_ID = "Vaadin-Security-Key";
/**
* Name of the parameter used to transmit root ids back and forth
*/
public static final String ROOT_ID_PARAMETER = "rootId";
/**
* @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";
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";
private final boolean debugLogging = false;
// 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(ApplicationConfiguration.VERSION)) {
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 [email protected]::hasActiveRequest()()
|| [email protected]::isExecutingDeferredCommands()();
});
var vi = [email protected]::getVersionInfo()();
if (vi) {
client.getVersionInfo = function() {
return vi;
}
}
client.getProfilingData = $entry(function() {
var pd = [
[email protected]::lastProcessingTime,
[email protected]::totalProcessingTime
];
pd = pd.concat([email protected]::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();
}
[email protected]::sendPendingVariableChanges()();
});
var oldForceLayout;
if ($wnd.vaadin.forceLayout) {
oldForceLayout = $wnd.vaadin.forceLayout;
}
$wnd.vaadin.forceLayout = $entry(function() {
if (oldForceLayout) {
oldForceLayout();
}
[email protected]::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;
if (configuration.usePortletURLs()) {
uri = configuration.getPortletUidlURLBase();
} else {
uri = getAppUri() + "UIDL";
}
if (extraParams != null && extraParams.length() > 0) {
uri = addGetParameters(uri, extraParams);
}
uri = addGetParameters(uri,
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() {
public void onError(Request request, Throwable exception) {
showCommunicationError(exception.getMessage(), -1);
endRequest();
}
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 + "&" + 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() {
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(UIDL_SECURITY_TOKEN_ID)) {
uidlSecurityKey = json.getString(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() {
public void execute() {
handleUIDLDuration.logDuration(" * Loading widgets completed",
10);
MultiStepDuration updateDuration = new MultiStepDuration();
if (debugLogging) {
VConsole.log(" * Dumping UIDL to the console");
VConsole.dirUIDL(json, configuration);
updateDuration.logDuration(
" * Dumping UIDL to the console completed", 10);
}
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 (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 extends ServerConnector> 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() {
public void onLoad(ResourceLoadEvent event) {
ApplicationConfiguration.endDependencyLoading();
}
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() {
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();
}
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,
UPDATE_VARIABLE_INTERFACE, 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() {
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 = ApplicationConfiguration.VERSION;
extraParams += "wsver=" + widgetsetVersion;
getConfiguration().setWidgetsetVersionSent();
}
makeUidlRequest(req.toString(), extraParams, forceSync);
}
private boolean isLegacyVariableChange(MethodInvocation invocation) {
return ApplicationConnection.UPDATE_VARIABLE_METHOD.equals(invocation
.getInterfaceName())
&& ApplicationConnection.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("app://")) {
uidlUri = getAppUri() + uidlUri.substring(6);
} else if (uidlUri.startsWith(CONNECTOR_PROTOCOL_PREFIX)) {
// getAppUri *should* always end with /
// substring *should* always start with / (connector:///foo.bar
// without connector://)
uidlUri = getAppUri() + CONNECTOR_RESOURCE_PREFIX
+ uidlUri.substring(CONNECTOR_PROTOCOL_PREFIX.length());
}
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;
}
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;
}
}