/*
* 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 <code>onModuleLoad()</code>.
*/
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.
*
* <p>
* This allows, for instance, a servlet filter to redirect the application
* to a custom login page when the session expires. For example:
* </p>
*
* <pre class='code'>
* if (sessionExpired) {
* response.setHeader("Content-Type", "text/html");
* response.getWriter().write(
* myLoginPageHtml + "<!-- Vaadin-Refresh: "
* + request.getContextPath() + " -->");
* }
* </pre>
*/
public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh";
// will hold the UIDL security key (for XSS protection) once received
private String uidlSecurityKey = "init";
private final HashMap<String, String> resourcesMap = new HashMap<String, String>();
private final ArrayList<String> pendingVariables = new ArrayList<String>();
private final ComponentDetailMap idToPaintableDetail = ComponentDetailMap
.create();
private WidgetSet widgetSet;
private VContextMenu contextMenu = null;
private Timer loadTimer;
private Timer loadTimer2;
private Timer loadTimer3;
private Element loadElement;
private final VView view;
protected boolean applicationRunning = false;
private int activeRequests = 0;
/** 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<ArrayList<String>> pendingVariableBursts = new ArrayList<ArrayList<String>>();
/** Timer for automatic refirect to SessionExpiredURL */
private Timer redirectTimer;
/** redirectTimer scheduling interval in seconds */
private int sessionExpirationInterval;
private ArrayList<Paintable> relativeSizeChanges = new ArrayList<Paintable>();;
private ArrayList<Paintable> componentCaptionSizeChanges = new ArrayList<Paintable>();;
private Date requestStartTime;
private boolean validatingLayouts = false;
private Set<Paintable> zeroWidthComponents = null;
private Set<Paintable> zeroHeightComponents = null;
private Set<String> unregistryBag = new HashSet<String>();
public ApplicationConnection() {
view = GWT.create(VView.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;
windowName = configuration.getInitialWindowName();
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();
view.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.
*
* @return
*/
public void start() {
repaintAll();
}
private native void initializeTestbenchHooks(
ComponentLocator componentLocator, String TTAppId)
/*-{
var ap = this;
var client = {};
client.isActive = function() {
return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()()
|| ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()();
}
var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()();
if (vi) {
client.getVersionInfo = function() {
return vi;
}
}
client.getElementByPath = function(id) {
return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
}
client.getPathForElement = function(element) {
return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
}
if (!$wnd.vaadin.clients) {
$wnd.vaadin.clients = {};
}
$wnd.vaadin.clients[TTAppId] = client;
}-*/;
/**
* Helper for tt initialization
*/
private JavaScriptObject getVersionInfo() {
return configuration.getVersionInfoJSObject();
}
/**
* Publishes a JavaScript API for mash-up applications.
* <ul>
* <li><code>vaadin.forceSync()</code> sends pending variable changes, in
* effect synchronizing the server and client state. This is done for all
* applications on host page.</li>
* <li><code>vaadin.postRequestHooks</code> 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:
*
* <code><pre>
* if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();}
* postRequestHooks.myHook = function(appId) {
* if(appId == "MyAppOfInterest") {
* // do the staff you need on xhr activity
* }
* }
* </pre></code> First parameter passed to these functions is the identifier
* of Vaadin application that made the request.
* </ul>
*
* 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 = function() {
if (oldSync) {
oldSync();
}
app.@com.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()();
}
var oldForceLayout;
if ($wnd.vaadin.forceLayout) {
oldForceLayout = $wnd.vaadin.forceLayout;
}
$wnd.vaadin.forceLayout = function() {
if (oldForceLayout) {
oldForceLayout();
}
app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()();
}
}-*/;
/**
* Runs possibly registered client side post request hooks. This is expected
* to be run after each uidl request made by Vaadin application.
*
* @param appId
*/
private static native void runPostRequestHooks(String appId)
/*-{
if ($wnd.vaadin.postRequestHooks) {
for ( var hook in $wnd.vaadin.postRequestHooks) {
if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") {
try {
$wnd.vaadin.postRequestHooks[hook](appId);
} catch (e) {
}
}
}
}
}-*/;
/**
* 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 squence requests properly, seldom needed in Widgets.
*
* @return true if there are active requests
*/
public boolean hasActiveRequest() {
return (activeRequests > 0);
}
private String getRepaintAllParameters() {
// collect some client side data that will be sent to server on
// initial uidl request
int clientHeight = Window.getClientHeight();
int clientWidth = Window.getClientWidth();
com.google.gwt.dom.client.Element pe = view.getElement()
.getParentElement();
int offsetHeight = pe.getOffsetHeight();
int offsetWidth = pe.getOffsetWidth();
int screenWidth = BrowserInfo.get().getScreenWidth();
int screenHeight = BrowserInfo.get().getScreenHeight();
int tzOffset = BrowserInfo.get().getTimezoneOffset();
int rtzOffset = BrowserInfo.get().getRawTimezoneOffset();
int dstDiff = BrowserInfo.get().getDSTSavings();
boolean dstInEffect = BrowserInfo.get().isDSTInEffect();
long curDate = BrowserInfo.get().getCurrentDate().getTime();
String widgetsetVersion = ApplicationConfiguration.VERSION;
String token = History.getToken();
// 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&" + "sh=" + screenHeight + "&sw="
+ screenWidth + "&cw=" + clientWidth + "&ch=" + clientHeight
+ "&vw=" + offsetWidth + "&vh=" + offsetHeight + "&fr=" + token
+ "&tzo=" + tzOffset + "&rtzo=" + rtzOffset + "&dstd="
+ dstDiff + "&dston=" + dstInEffect + "&curdate=" + curDate
+ "&wsver=" + widgetsetVersion
+ (BrowserInfo.get().isTouchDevice() ? "&td=1" : "");
return parameters;
}
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 paintable
*/
void highlightComponent(Paintable paintable) {
String params = getRepaintAllParameters() + "&highlightComponent="
+ getPid(paintable);
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);
}
if (windowName != null && windowName.length() > 0) {
uri = addGetParameters(uri, "windowName=" + windowName);
}
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();
if (!applicationRunning) {
// start failed, let's try to start the next app
ApplicationConfiguration.startNextApplication();
}
}
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
int delay = Integer.parseInt(response
.getHeader("Retry-After"));
VConsole.log("503, retrying in " + delay + "msec");
(new Timer() {
@Override
public void run() {
activeRequests--;
doUidlRequest(uri, payload, synchronous);
}
}).schedule(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;
}
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;
}
}
final Date start = new Date();
// for(;;);[realjson]
final String jsonText = response.getText().substring(9,
response.getText().length() - 1);
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);
ApplicationConfiguration.startNextApplication();
}
}
};
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();
}
}
/**
* 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;
static final int MAX_CSS_WAITS = 100;
protected void handleWhenCSSLoaded(final String jsonText,
final ValueMap json) {
int heightOfLoadElement = DOM.getElementPropertyInt(loadElement,
"offsetHeight");
if (heightOfLoadElement == 0 && 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 {
handleReceivedJSONMessage(new Date(), jsonText, json);
if (cssWaits >= MAX_CSS_WAITS) {
VConsole.error("CSS files may have not loaded properly.");
}
}
}
/**
* Shows the communication error notification.
*
* @param details
* Optional details for debugging.
*
* @deprecated Use the method with errorCode instead
*/
@Deprecated
protected void showCommunicationError(String details) {
VConsole.error("Communication error: " + details);
showError(details, configuration.getCommunicationErrorCaption(),
configuration.getCommunicationErrorMessage(),
configuration.getCommunicationErrorUrl());
}
/**
* Shows the communication error notification.
*
* @param details
* Optional details for debugging.
* @param statusCode
* The http error code during the problematic request or -1 if
* error happened before response was received.
*/
protected void showCommunicationError(String details, int statusCode) {
showAuthenticationError(details);
}
/**
* Shows the authentication error notification.
*
* @param details
* Optional details for debugging.
*/
protected void showAuthenticationError(String details) {
VConsole.error("Authentication error: " + details);
showError(details, configuration.getAuthorizationErrorCaption(),
configuration.getAuthorizationErrorMessage(),
configuration.getAuthorizationErrorUrl());
}
/**
* 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("<h1>");
html.append(caption);
html.append("</h1>");
}
if (message != null) {
html.append("<p>");
html.append(message);
html.append("</p>");
}
if (html.length() > 0) {
// Add error description
html.append("<br/><p><I style=\"font-size:0.7em\">");
html.append(details);
html.append("</I></p>");
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() {
activeRequests++;
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 (applicationRunning) {
checkForPendingVariableBursts();
runPostRequestHooks(configuration.getRootPanelId());
}
activeRequests--;
// deferring to avoid flickering
Scheduler.get().scheduleDeferred(new Command() {
public void execute() {
if (activeRequests == 0) {
hideLoadingIndicator();
}
}
});
}
/**
* 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<ArrayList<String>> iterator = pendingVariableBursts
.iterator(); iterator.hasNext();) {
cleanVariableBurst(iterator.next());
}
ArrayList<String> nextBurst = pendingVariableBursts.get(0);
pendingVariableBursts.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<String> variableBurst) {
for (int i = 1; i < variableBurst.size(); i += 2) {
String id = variableBurst.get(i);
id = id.substring(0, id.indexOf(VAR_FIELD_SEPARATOR));
if (!idToPaintableDetail.containsKey(id) && !id.startsWith("DD")) {
// variable owner does not exist anymore
variableBurst.remove(i - 1);
variableBurst.remove(i - 1);
i -= 2;
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(view.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();
if (loadTimer2 != null) {
loadTimer2.cancel();
loadTimer3.cancel();
}
loadTimer = 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.
* <p>
* Used by the native "client.isActive" function.
* </p>
*
* @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);
}
Command c = new Command() {
public void execute() {
VConsole.dirUIDL(json, configuration);
if (json.containsKey("locales")) {
// Store locale data
JsArray<ValueMap> valueMapArray = json
.getJSValueMapArray("locales");
LocaleService.addLocales(valueMapArray);
}
boolean repaintAll = false;
ValueMap meta = null;
if (json.containsKey("meta")) {
meta = json.getValueMap("meta");
if (meta.containsKey("repaintAll")) {
repaintAll = true;
view.clear();
idToPaintableDetail.clear();
if (meta.containsKey("invalidLayouts")) {
validatingLayouts = true;
zeroWidthComponents = new HashSet<Paintable>();
zeroHeightComponents = new HashSet<Paintable>();
}
}
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");
}
}
if (redirectTimer != null) {
redirectTimer.schedule(1000 * sessionExpirationInterval);
}
// Process changes
JsArray<ValueMap> changes = json.getJSValueMapArray("changes");
ArrayList<Paintable> updatedWidgets = new ArrayList<Paintable>();
relativeSizeChanges.clear();
componentCaptionSizeChanges.clear();
int length = changes.length();
for (int i = 0; i < length; i++) {
try {
final UIDL change = changes.get(i).cast();
final UIDL uidl = change.getChildUIDL(0);
// TODO optimize
final Paintable paintable = getPaintable(uidl.getId());
if (paintable != null) {
paintable.updateFromUIDL(uidl,
ApplicationConnection.this);
// paintable may have changed during render to
// another
// implementation, use the new one for updated
// widgets map
updatedWidgets.add(idToPaintableDetail.get(
uidl.getId()).getComponent());
} else {
if (!uidl.getTag().equals(
configuration.getEncodedWindowTag())) {
VConsole.error("Received update for "
+ uidl.getTag()
+ ", but there is no such paintable ("
+ uidl.getId() + ") rendered.");
} else {
String pid = uidl.getId();
if (!idToPaintableDetail.containsKey(pid)) {
registerPaintable(pid, view);
}
// VView does not call updateComponent so we
// register any event listeners here
ComponentDetail cd = idToPaintableDetail
.get(pid);
cd.registerEventListenersFromUIDL(uidl);
// Finally allow VView to update itself
view.updateFromUIDL(uidl,
ApplicationConnection.this);
}
}
} catch (final Throwable e) {
VConsole.error(e);
}
}
if (json.containsKey("dd")) {
// response contains data for drag and drop service
VDragAndDropManager.get().handleServerResponse(
json.getValueMap("dd"));
}
// Check which widgets' size has been updated
Set<Paintable> sizeUpdatedWidgets = new HashSet<Paintable>();
updatedWidgets.addAll(relativeSizeChanges);
sizeUpdatedWidgets.addAll(componentCaptionSizeChanges);
for (Paintable paintable : updatedWidgets) {
ComponentDetail detail = idToPaintableDetail
.get(getPid(paintable));
Widget widget = (Widget) paintable;
Size oldSize = detail.getOffsetSize();
Size newSize = new Size(widget.getOffsetWidth(),
widget.getOffsetHeight());
if (oldSize == null || !oldSize.equals(newSize)) {
sizeUpdatedWidgets.add(paintable);
detail.setOffsetSize(newSize);
}
}
Util.componentSizeUpdated(sizeUpdatedWidgets);
if (meta != null) {
if (meta.containsKey("appError")) {
ValueMap error = meta.getValueMap("appError");
String html = "";
if (error.containsKey("caption")
&& error.getString("caption") != null) {
html += "<h1>" + error.getAsString("caption")
+ "</h1>";
}
if (error.containsKey("message")
&& error.getString("message") != null) {
html += "<p>" + error.getAsString("message")
+ "</p>";
}
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
final long prosessingTime = (new Date().getTime())
- start.getTime();
VConsole.log(" Processing time was "
+ String.valueOf(prosessingTime) + "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<String> nextBurst = pendingVariableBursts.get(0);
pendingVariableBursts.remove(0);
buildAndSendVariableBurst(nextBurst, true);
}
}
// Redirect browser, null reloads current page
private static native void redirect(String url)
/*-{
if (url) {
$wnd.location = url;
} else {
$wnd.location.reload(false);
}
}-*/;
public void registerPaintable(String pid, Paintable paintable) {
ComponentDetail componentDetail = new ComponentDetail(this, pid,
paintable);
idToPaintableDetail.put(pid, componentDetail);
setPid(((Widget) paintable).getElement(), pid);
}
private native void setPid(Element el, String pid)
/*-{
el.tkPid = pid;
}-*/;
/**
* Gets the paintableId for a specific paintable (a.k.a Vaadin Widget).
* <p>
* The paintableId is used in the UIDL to identify a specific widget
* instance, effectively linking the widget with it's server side Component.
* </p>
*
* @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<Widget> it = container.iterator();
while (it.hasNext()) {
final Widget w = it.next();
if (w instanceof Paintable) {
unregisterPaintable((Paintable) w);
} else if (w instanceof HasWidgets) {
unregisterChildPaintables((HasWidgets) w);
}
}
}
/**
* Returns Paintable element by its id
*
* @param id
* Paintable ID
*/
public Paintable getPaintable(String id) {
ComponentDetail componentDetail = idToPaintableDetail.get(id);
if (componentDetail == null) {
return null;
} else {
return componentDetail.getComponent();
}
}
private void addVariableToQueue(String paintableId, String variableName,
String encodedValue, boolean immediate, char type) {
final String id = paintableId + VAR_FIELD_SEPARATOR + variableName
+ VAR_FIELD_SEPARATOR + type;
for (int i = 1; i < pendingVariables.size(); i += 2) {
if ((pendingVariables.get(i)).equals(id)) {
pendingVariables.remove(i - 1);
pendingVariables.remove(i - 1);
break;
}
}
pendingVariables.add(encodedValue);
pendingVariables.add(id);
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.
*
*/
@SuppressWarnings("unchecked")
public void sendPendingVariableChanges() {
if (applicationRunning) {
if (hasActiveRequest()) {
// skip empty queues if there are pending bursts to be sent
if (pendingVariables.size() > 0
|| pendingVariableBursts.size() == 0) {
ArrayList<String> burst = (ArrayList<String>) pendingVariables
.clone();
pendingVariableBursts.add(burst);
pendingVariables.clear();
}
} else {
buildAndSendVariableBurst(pendingVariables, 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 pendingVariables
* Vector of variable changes to send
* @param forceSync
* Should we use synchronous request?
*/
private void buildAndSendVariableBurst(ArrayList<String> pendingVariables,
boolean forceSync) {
final StringBuffer req = new StringBuffer();
while (!pendingVariables.isEmpty()) {
if (ApplicationConfiguration.isDebugMode()) {
Util.logVariableBurst(this, pendingVariables);
}
for (int i = 0; i < pendingVariables.size(); i++) {
if (i > 0) {
if (i % 2 == 0) {
req.append(VAR_RECORD_SEPARATOR);
} else {
req.append(VAR_FIELD_SEPARATOR);
}
}
req.append(pendingVariables.get(i));
}
pendingVariables.clear();
// Append all the busts to this synchronous request
if (forceSync && !pendingVariableBursts.isEmpty()) {
pendingVariables = pendingVariableBursts.get(0);
pendingVariableBursts.remove(0);
req.append(VAR_BURST_SEPARATOR);
}
}
makeUidlRequest(req.toString(), "", forceSync);
}
private void makeUidlRequest(String string) {
makeUidlRequest(string, "", false);
}
/**
* Sends a new value for the given paintables given variable to the server.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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<String, Object> map, boolean immediate) {
final StringBuffer buf = new StringBuffer();
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
Object value = map.get(key);
char transportType = getTransportType(value);
buf.append(transportType);
buf.append(escapeVariableValue(key));
buf.append(VAR_ARRAYITEM_SEPARATOR);
if (transportType == 'p') {
buf.append(getPid((Paintable) value));
} else {
buf.append(escapeVariableValue(String.valueOf(value)));
}
if (iterator.hasNext()) {
buf.append(VAR_ARRAYITEM_SEPARATOR);
}
}
addVariableToQueue(paintableId, variableName, buf.toString(),
immediate, 'm');
}
private char getTransportType(Object value) {
if (value instanceof String) {
return 's';
} else if (value instanceof Paintable) {
return 'p';
} else if (value instanceof Boolean) {
return 'b';
} else if (value instanceof Integer) {
return 'i';
} else if (value instanceof Float) {
return 'f';
} else if (value instanceof Double) {
return 'd';
} else if (value instanceof Long) {
return 'l';
} else if (value instanceof Enum) {
return 's'; // transported as string representation
}
return 'u';
}
/**
* 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 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[] values, boolean immediate) {
final StringBuffer buf = new StringBuffer();
if (values != null) {
for (int i = 0; i < values.length; i++) {
buf.append(escapeVariableValue(values[i]));
// there will be an extra separator at the end to differentiate
// between an empty array and one containing an empty string
// only
buf.append(VAR_ARRAYITEM_SEPARATOR);
}
}
addVariableToQueue(paintableId, variableName, buf.toString(),
immediate, 'c');
}
/**
* 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. </p>
*
* 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 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,
Object[] values, boolean immediate) {
final StringBuffer buf = new StringBuffer();
if (values != null) {
for (int i = 0; i < values.length; i++) {
if (i > 0) {
buf.append(VAR_ARRAYITEM_SEPARATOR);
}
Object value = values[i];
char transportType = getTransportType(value);
// first char tells the type in array
buf.append(transportType);
if (transportType == 'p') {
buf.append(getPid((Paintable) value));
} else {
buf.append(escapeVariableValue(String.valueOf(value)));
}
}
}
addVariableToQueue(paintableId, variableName, buf.toString(),
immediate, 'a');
}
/**
* Encode burst, record, field and array item 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 escapeVariableValue(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:
case VAR_RECORD_SEPARATOR:
case VAR_FIELD_SEPARATOR:
case VAR_ARRAYITEM_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();
}
/**
* Update generic component features.
*
* <h2>Selecting correct implementation</h2>
*
* <p>
* 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.
* </p>
*
* <h2>Caption, icon, error messages and description</h2>
*
* <p>
* Component can delegate management of caption, icon, error messages and
* description to parent layout. This is optional an should be decided by
* component author
* </p>
*
* <h2>Component visibility and disabling</h2>
*
* This method will manage component visibility automatically and if
* component is an instanceof FocusWidget, also handle component disabling
* when needed.
*
* @param component
* Widget to be updated, expected to implement an instance of
* Paintable
* @param uidl
* UIDL to be painted
* @param manageCaption
* True if you want to delegate caption, icon, description and
* error message management to parent.
*
* @return Returns true iff no further painting is needed by caller
*/
public boolean updateComponent(Widget component, UIDL uidl,
boolean manageCaption) {
String pid = getPid(component.getElement());
if (pid == null) {
VConsole.error("Trying to update an unregistered component: "
+ Util.getSimpleName(component));
return true;
}
ComponentDetail componentDetail = idToPaintableDetail.get(pid);
if (componentDetail == null) {
VConsole.error("ComponentDetail not found for "
+ Util.getSimpleName(component) + " with PID " + pid
+ ". This should not happen.");
return true;
}
// If the server request that a cached instance should be used, do
// nothing
if (uidl.getBooleanAttribute("cached")) {
return true;
}
// register the listened events by the server-side to the event-handler
// of the component
componentDetail.registerEventListenersFromUIDL(uidl);
// Visibility
boolean visible = !uidl.getBooleanAttribute("invisible");
boolean wasVisible = component.isVisible();
component.setVisible(visible);
if (wasVisible != visible) {
// Changed invisibile <-> visible
if (wasVisible && manageCaption) {
// Must hide caption when component is hidden
final Container parent = Util.getLayout(component);
if (parent != null) {
parent.updateCaption((Paintable) component, uidl);
}
}
}
if (configuration.useDebugIdInDOM() && uidl.getId().startsWith("PID_S")) {
DOM.setElementProperty(component.getElement(), "id", uidl.getId()
.substring(5));
}
if (!visible) {
// component is invisible, delete old size to notify parent, if
// later make visible
componentDetail.setOffsetSize(null);
return true;
}
// Switch to correct implementation if needed
if (!widgetSet.isCorrectImplementation(component, uidl, configuration)) {
final Widget w = (Widget) widgetSet.createWidget(uidl,
configuration);
// deferred binding check TODO change isCorrectImplementation to use
// stored detected class, making this innecessary
if (w.getClass() != component.getClass()) {
final Container parent = Util.getLayout(component);
if (parent != null) {
parent.replaceChildComponent(component, w);
unregisterPaintable((Paintable) component);
registerPaintable(uidl.getId(), (Paintable) w);
((Paintable) w).updateFromUIDL(uidl, this);
return true;
}
}
}
boolean enabled = !uidl.getBooleanAttribute("disabled");
if (uidl.hasAttribute("tabindex") && component instanceof Focusable) {
((Focusable) component).setTabIndex(uidl
.getIntAttribute("tabindex"));
}
/*
* Disabled state may affect (override) tabindex so the order must be
* first setting tabindex, then enabled state.
*/
if (component instanceof FocusWidget) {
FocusWidget fw = (FocusWidget) component;
fw.setEnabled(enabled);
}
TooltipInfo tooltipInfo = componentDetail.getTooltipInfo(null);
// Update tooltip
if (uidl.hasAttribute(ATTRIBUTE_DESCRIPTION)) {
tooltipInfo
.setTitle(uidl.getStringAttribute(ATTRIBUTE_DESCRIPTION));
} else {
tooltipInfo.setTitle(null);
}
// add error classname to components w/ error
if (uidl.hasAttribute(ATTRIBUTE_ERROR)) {
tooltipInfo.setErrorUidl(uidl.getErrors());
} else {
tooltipInfo.setErrorUidl(null);
}
// Style names
component.setStyleName(getStyleName(component.getStylePrimaryName(),
uidl, component instanceof Field));
// Set captions
if (manageCaption) {
final Container parent = Util.getLayout(component);
if (parent != null) {
parent.updateCaption((Paintable) component, uidl);
}
}
/*
* updateComponentSize need to be after caption update so caption can be
* taken into account
*/
updateComponentSize(componentDetail, uidl);
return false;
}
/**
* Generates the style name for the widget based on the given primary style
* name (typically returned by Widget.getPrimaryStyleName()) and the UIDL.
* An additional "modified" style name can be added if the field parameter
* is set to true.
*
* @param primaryStyleName
* @param uidl
* @param isField
* @return
*/
public static String getStyleName(String primaryStyleName, UIDL uidl,
boolean field) {
boolean enabled = !uidl.getBooleanAttribute("disabled");
StringBuffer styleBuf = new StringBuffer();
styleBuf.append(primaryStyleName);
// first disabling and read-only status
if (!enabled) {
styleBuf.append(" ");
styleBuf.append(DISABLED_CLASSNAME);
}
if (uidl.getBooleanAttribute("readonly")) {
styleBuf.append(" ");
styleBuf.append("v-readonly");
}
// add additional styles as css classes, prefixed with component default
// stylename
if (uidl.hasAttribute("style")) {
final String[] styles = uidl.getStringAttribute("style").split(" ");
for (int i = 0; i < styles.length; i++) {
styleBuf.append(" ");
styleBuf.append(primaryStyleName);
styleBuf.append("-");
styleBuf.append(styles[i]);
styleBuf.append(" ");
styleBuf.append(styles[i]);
}
}
// add modified classname to Fields
if (field && uidl.hasAttribute("modified")) {
styleBuf.append(" ");
styleBuf.append(MODIFIED_CLASSNAME);
}
if (uidl.hasAttribute(ATTRIBUTE_ERROR)) {
styleBuf.append(" ");
styleBuf.append(primaryStyleName);
styleBuf.append(ERROR_CLASSNAME_EXT);
}
// add required style to required components
if (uidl.hasAttribute("required")) {
styleBuf.append(" ");
styleBuf.append(primaryStyleName);
styleBuf.append(REQUIRED_CLASSNAME_EXT);
}
return styleBuf.toString();
}
private void updateComponentSize(ComponentDetail cd, UIDL uidl) {
String w = uidl.hasAttribute("width") ? uidl
.getStringAttribute("width") : "";
String h = uidl.hasAttribute("height") ? uidl
.getStringAttribute("height") : "";
float relativeWidth = Util.parseRelativeSize(w);
float relativeHeight = Util.parseRelativeSize(h);
// First update maps so they are correct in the setHeight/setWidth calls
if (relativeHeight >= 0.0 || relativeWidth >= 0.0) {
// One or both is relative
FloatSize relativeSize = new FloatSize(relativeWidth,
relativeHeight);
if (cd.getRelativeSize() == null && cd.getOffsetSize() != null) {
// The component has changed from absolute size to relative size
relativeSizeChanges.add(cd.getComponent());
}
cd.setRelativeSize(relativeSize);
} else if (relativeHeight < 0.0 && relativeWidth < 0.0) {
if (cd.getRelativeSize() != null) {
// The component has changed from relative size to absolute size
relativeSizeChanges.add(cd.getComponent());
}
cd.setRelativeSize(null);
}
Widget component = (Widget) cd.getComponent();
// Set absolute sizes
if (relativeHeight < 0.0) {
component.setHeight(h);
}
if (relativeWidth < 0.0) {
component.setWidth(w);
}
// Set relative sizes
if (relativeHeight >= 0.0 || relativeWidth >= 0.0) {
// One or both is relative
handleComponentRelativeSize(cd);
}
}
/**
* Traverses recursively child widgets until ContainerResizedListener child
* widget is found. They will delegate it further if needed.
*
* @param container
*/
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() {
Set<Paintable> set = new HashSet<Paintable>();
for (ComponentDetail cd : idToPaintableDetail.values()) {
set.add(cd.getComponent());
}
Util.componentSizeUpdated(set);
}
private void internalRunDescendentsLayout(HasWidgets container) {
// getConsole().log(
// "runDescendentsLayout(" + Util.getSimpleName(container) + ")");
final Iterator<Widget> childWidgets = container.iterator();
while (childWidgets.hasNext()) {
final Widget child = childWidgets.next();
if (child instanceof Paintable) {
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(ComponentDetail cd) {
if (cd == null) {
return false;
}
boolean debugSizes = false;
FloatSize relativeSize = cd.getRelativeSize();
if (relativeSize == null) {
return false;
}
Widget widget = (Widget) cd.getComponent();
boolean horizontalScrollBar = false;
boolean verticalScrollBar = false;
Container parent = Util.getLayout(widget);
RenderSpace renderSpace;
// Parent-less components (like sub-windows) are relative to browser
// window.
if (parent == null) {
renderSpace = new RenderSpace(Window.getClientWidth(),
Window.getClientHeight());
} else {
renderSpace = parent.getAllocatedSpace(widget);
}
if (relativeSize.getHeight() >= 0) {
if (renderSpace != null) {
if (renderSpace.getScrollbarSize() > 0) {
if (relativeSize.getWidth() > 100) {
horizontalScrollBar = true;
} else if (relativeSize.getWidth() < 0
&& renderSpace.getWidth() > 0) {
int offsetWidth = widget.getOffsetWidth();
int width = renderSpace.getWidth();
if (offsetWidth > width) {
horizontalScrollBar = true;
}
}
}
int height = renderSpace.getHeight();
if (horizontalScrollBar) {
height -= renderSpace.getScrollbarSize();
}
if (validatingLayouts && height <= 0) {
zeroHeightComponents.add(cd.getComponent());
}
height = (int) (height * relativeSize.getHeight() / 100.0);
if (height < 0) {
height = 0;
}
if (debugSizes) {
VConsole.log("Widget " + Util.getSimpleName(widget) + "/"
+ getPid(widget.getElement()) + " relative height "
+ relativeSize.getHeight() + "% of "
+ renderSpace.getHeight() + "px (reported by "
+ Util.getSimpleName(parent) + "/"
+ (parent == null ? "?" : parent.hashCode())
+ ") : " + height + "px");
}
widget.setHeight(height + "px");
} else {
widget.setHeight(relativeSize.getHeight() + "%");
VConsole.error(Util.getLayout(widget).getClass().getName()
+ " did not produce allocatedSpace for "
+ widget.getClass().getName());
}
}
if (relativeSize.getWidth() >= 0) {
if (renderSpace != null) {
int width = renderSpace.getWidth();
if (renderSpace.getScrollbarSize() > 0) {
if (relativeSize.getHeight() > 100) {
verticalScrollBar = true;
} else if (relativeSize.getHeight() < 0
&& renderSpace.getHeight() > 0
&& widget.getOffsetHeight() > renderSpace
.getHeight()) {
verticalScrollBar = true;
}
}
if (verticalScrollBar) {
width -= renderSpace.getScrollbarSize();
}
if (validatingLayouts && width <= 0) {
zeroWidthComponents.add(cd.getComponent());
}
width = (int) (width * relativeSize.getWidth() / 100.0);
if (width < 0) {
width = 0;
}
if (debugSizes) {
VConsole.log("Widget " + Util.getSimpleName(widget) + "/"
+ getPid(widget.getElement()) + " relative width "
+ relativeSize.getWidth() + "% of "
+ renderSpace.getWidth() + "px (reported by "
+ Util.getSimpleName(parent) + "/"
+ (parent == null ? "?" : getPid(parent)) + ") : "
+ width + "px");
}
widget.setWidth(width + "px");
} else {
widget.setWidth(relativeSize.getWidth() + "%");
VConsole.error(Util.getLayout(widget).getClass().getName()
+ " did not produce allocatedSpace for "
+ widget.getClass().getName());
}
}
return true;
}
/**
* Converts relative sizes into pixel sizes.
*
* @param child
* @return true if the child has a relative size
*/
public boolean handleComponentRelativeSize(Widget child) {
return handleComponentRelativeSize(idToPaintableDetail.get(getPid(child
.getElement())));
}
/**
* Gets the specified Paintables relative size (percent).
*
* @param widget
* the paintable whose size is needed
* @return the the size if the paintable is relatively sized, -1 otherwise
*/
public FloatSize getRelativeSize(Widget widget) {
return idToPaintableDetail.get(getPid(widget.getElement()))
.getRelativeSize();
}
/**
* Get either existing or new Paintable for given UIDL.
*
* If corresponding Paintable has been previously painted, return it.
* Otherwise create and register a new Paintable from UIDL. Caller must
* update the returned Paintable from UIDL after it has been connected to
* parent.
*
* @param uidl
* UIDL to create Paintable from.
* @return Either existing or new Paintable corresponding to UIDL.
*/
public Paintable getPaintable(UIDL uidl) {
final String id = uidl.getId();
Paintable w = getPaintable(id);
if (w != null) {
return w;
} else {
w = widgetSet.createWidget(uidl, configuration);
registerPaintable(id, w);
return w;
}
}
/**
* Returns a Paintable element by its root element
*
* @param element
* Root element of the paintable
*/
public Paintable getPaintable(Element element) {
return getPaintable(getPid(element));
}
/**
* 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);
}
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 */
/**
* Data showed in tooltips are stored centrilized as it may be needed in
* varios place: caption, layouts, and in owner components themselves.
*
* Updating TooltipInfo is done in updateComponent method.
*
*/
public TooltipInfo getTooltipTitleInfo(Paintable titleOwner, Object key) {
if (null == titleOwner) {
return null;
}
ComponentDetail cd = idToPaintableDetail.get(getPid(titleOwner));
if (null != cd) {
return cd.getTooltipInfo(key);
} else {
return null;
}
}
private final VTooltip tooltip = new VTooltip(this);
/**
* Component may want to delegate Tooltip handling to client. Layouts add
* Tooltip (description, errors) to caption, but some components may want
* them to appear one other elements too.
*
* Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS
*
* @param event
* @param owner
*/
public void handleTooltipEvent(Event event, Paintable owner) {
tooltip.handleTooltipEvent(event, owner, null);
}
/**
* Component may want to delegate Tooltip handling to client. Layouts add
* Tooltip (description, errors) to caption, but some components may want
* them to appear one other elements too.
*
* Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS
*
* @param event
* @param owner
* @param key
* the key for tooltip if this is "additional" tooltip, null for
* components "main tooltip"
*/
public void handleTooltipEvent(Event event, Paintable owner, Object key) {
tooltip.handleTooltipEvent(event, owner, key);
}
/**
* Adds PNG-fix conditionally (only for IE6) to the specified IMG -element.
*
* @param el
* the IMG element to fix
*/
public void addPngFix(Element el) {
BrowserInfo b = BrowserInfo.get();
if (b.isIE6()) {
Util.addPngFix(el);
}
}
/*
* Helper to run layout functions triggered by child components with a
* decent interval.
*/
private final Timer layoutTimer = new Timer() {
private boolean isPending = false;
@Override
public void schedule(int delayMillis) {
if (!isPending) {
super.schedule(delayMillis);
isPending = true;
}
}
@Override
public void run() {
VConsole.log("Running re-layout of " + view.getClass().getName());
runDescendentsLayout(view);
isPending = false;
}
};
/**
* Components can call this function to run all layout functions. This is
* usually done, when component knows that its size has changed.
*/
public void requestLayoutPhase() {
layoutTimer.schedule(500);
}
private String windowName = null;
/**
* Reset the name of the current browser-window. This should reflect the
* window-name used in the server, but might be different from the
* window-object target-name on client.
*
* @param stringAttribute
* New name for the window.
*/
public void setWindowName(String newName) {
windowName = newName;
}
protected String getWindowName() {
return windowName;
}
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(Paintable component) {
componentCaptionSizeChanges.add(component);
}
/**
* Gets the main view, a.k.a top-level window.
*
* @return the main view
*/
public VView getView() {
return view;
}
/**
* If component has several tooltips in addition to the one provided by
* {@link com.vaadin.ui.AbstractComponent}, component can register them with
* this method.
* <p>
* Component must also pipe events to
* {@link #handleTooltipEvent(Event, Paintable, Object)} method.
* <p>
* 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;
}
}