/*
@VaadinApache2LicenseForJavaFiles@
*/
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.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.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.ApplicationConfiguration.ErrorMessage;
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 VPaintableWidget#updateFromUIDL(UIDL, ApplicationConnection)} (not to
* be confused with the server side interface
* {@link com.vaadin.terminal.Paintable} ). Any client-side changes (typically
* resulting from user actions) are sent back to the server as variable changes
* (see {@link #updateVariable()}).
*
* TODO document better
*
* Entry point classes (widgetsets) define onModuleLoad()
.
*/
public class ApplicationConnection {
// This indicates the whole page is generated by us (not embedded)
public static final String GENERATED_BODY_CLASSNAME = "v-generated-body";
private static final String MODIFIED_CLASSNAME = "v-modified";
public static final String DISABLED_CLASSNAME = "v-disabled";
private static final String REQUIRED_CLASSNAME_EXT = "-required";
private static final String ERROR_CLASSNAME_EXT = "-error";
public static final char VAR_RECORD_SEPARATOR = '\u001e';
public static final char VAR_FIELD_SEPARATOR = '\u001f';
public static final char VAR_BURST_SEPARATOR = '\u001d';
public static final char VAR_ARRAYITEM_SEPARATOR = '\u001c';
public static final char VAR_ESCAPE_CHARACTER = '\u001b';
public static final String UIDL_SECURITY_TOKEN_ID = "Vaadin-Security-Key";
/**
* 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";
public static final String ATTRIBUTE_DESCRIPTION = "description";
public static final String ATTRIBUTE_ERROR = "error";
// will hold the UIDL security key (for XSS protection) once received
private String uidlSecurityKey = "init";
private final HashMap resourcesMap = new HashMap();
private final ArrayList pendingVariables = new ArrayList();
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;
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> pendingVariableBursts = new ArrayList>();
/** Timer for automatic refirect to SessionExpiredURL */
private Timer redirectTimer;
/** redirectTimer scheduling interval in seconds */
private int sessionExpirationInterval;
private ArrayList relativeSizeChanges = new ArrayList();
private ArrayList componentCaptionSizeChanges = new ArrayList();
private Date requestStartTime;
private boolean validatingLayouts = false;
private Set zeroWidthComponents = null;
private Set zeroHeightComponents = null;
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;
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.
*
*/
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
incrementActiveRequests();
// initial UIDL provided in DOM, continue as if returned by request
handleJSONText(jsonText);
}
}
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);
}
$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 = 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);
}
public void incrementActiveRequests() {
if (activeRequests < 0) {
activeRequests = 1;
} else {
activeRequests++;
}
}
public void decrementActiveRequests() {
if (activeRequests > 0) {
activeRequests--;
}
}
private String getRepaintAllParameters() {
// collect some client side data that will be sent to server on
// initial uidl request
String nativeBootstrapParameters = getNativeBrowserDetailsParameters(getConfiguration()
.getRootPanelId());
String widgetsetVersion = ApplicationConfiguration.VERSION;
// 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
+ "&wsver=" + widgetsetVersion;
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 paintable
*/
void highlightComponent(VPaintableWidget paintable) {
String params = getRepaintAllParameters() + "&highlightComponent="
+ paintableMap.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);
}
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());
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?)");
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() {
decrementActiveRequests();
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);
endRequest();
return;
}
// for(;;);[realjson]
final String jsonText = response.getText().substring(9,
response.getText().length() - 1);
handleJSONText(jsonText);
}
};
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
*/
private void handleJSONText(String jsonText) {
final Date start = new Date();
final ValueMap json;
try {
json = parseJSONResponse(jsonText);
} catch (final Exception e) {
endRequest();
showCommunicationError(e.getMessage() + " - Original JSON-text:"
+ jsonText);
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;
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.
*/
protected void showCommunicationError(String details) {
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() {
incrementActiveRequests();
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());
}
decrementActiveRequests();
// deferring to avoid flickering
Scheduler.get().scheduleDeferred(new Command() {
public void execute() {
if (!hasActiveRequest()) {
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> iterator = pendingVariableBursts
.iterator(); iterator.hasNext();) {
cleanVariableBurst(iterator.next());
}
ArrayList 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 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 (!getPaintableMap().hasPaintable(id)
&& !getPaintableMap().isDragAndDropPaintable(id)) {
// 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();
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) {
// 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 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();
getPaintableMap().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");
}
}
if (redirectTimer != null) {
redirectTimer.schedule(1000 * sessionExpirationInterval);
}
// Process changes
JsArray changes = json.getJSValueMapArray("changes");
ArrayList updatedVPaintableWidgets = new ArrayList();
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 VPaintableWidget paintable = (VPaintableWidget) paintableMap
.getPaintable(uidl.getId());
if (paintable != null) {
paintable.updateFromUIDL(uidl,
ApplicationConnection.this);
updatedVPaintableWidgets.add(paintable);
} 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 (!paintableMap.hasPaintable(pid)) {
paintableMap.registerPaintable(pid, view);
}
// VView does not call updateComponent so we
// register any event listeners here
paintableMap.registerEventListenersFromUIDL(
pid, 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 sizeUpdatedWidgets = new HashSet();
updatedVPaintableWidgets.addAll(relativeSizeChanges);
sizeUpdatedWidgets.addAll(componentCaptionSizeChanges);
for (VPaintableWidget paintable : updatedVPaintableWidgets) {
Widget widget = paintable.getWidgetForPaintable();
Size oldSize = paintableMap.getOffsetSize(paintable);
Size newSize = new Size(widget.getOffsetWidth(),
widget.getOffsetHeight());
if (oldSize == null || !oldSize.equals(newSize)) {
sizeUpdatedWidgets.add(widget);
paintableMap.setOffsetSize(paintable, 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 += "" + 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;
}
}
if (repaintAll) {
/*
* idToPaintableDetail is already cleanded at the start of
* the changeset handling, bypass cleanup.
*/
paintableMap.purgeUnregistryBag(false);
} else {
paintableMap.purgeUnregistryBag(true);
}
// 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: " + paintableMap.size());
endRequest();
}
};
ApplicationConfiguration.runWhenWidgetsLoaded(c);
}
// 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 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 burst = (ArrayList) 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 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);
}
}
// Include the browser detail parameters if they aren't already sent
String extraParams;
if (!getConfiguration().isBrowserDetailsSent()) {
extraParams = getNativeBrowserDetailsParameters(getConfiguration()
.getRootPanelId());
getConfiguration().setBrowserDetailsSent();
} else {
extraParams = "";
}
makeUidlRequest(req.toString(), extraParams, forceSync);
}
private void makeUidlRequest(String string) {
makeUidlRequest(string, "", false);
}
/**
* 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,
VPaintable newValue, boolean immediate) {
String pid = paintableMap.getPid(newValue);
addVariableToQueue(paintableId, variableName, pid, immediate, 'p');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param newValue
* the new value to be sent
* @param immediate
* true if the update is to be sent as soon as possible
*/
public void updateVariable(String paintableId, String variableName,
String newValue, boolean immediate) {
addVariableToQueue(paintableId, variableName,
escapeVariableValue(newValue), immediate, 's');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param newValue
* the new value to be sent
* @param immediate
* true if the update is to be sent as soon as possible
*/
public void updateVariable(String paintableId, String variableName,
int newValue, boolean immediate) {
addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
'i');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param newValue
* the new value to be sent
* @param immediate
* true if the update is to be sent as soon as possible
*/
public void updateVariable(String paintableId, String variableName,
long newValue, boolean immediate) {
addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
'l');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param newValue
* the new value to be sent
* @param immediate
* true if the update is to be sent as soon as possible
*/
public void updateVariable(String paintableId, String variableName,
float newValue, boolean immediate) {
addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
'f');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param newValue
* the new value to be sent
* @param immediate
* true if the update is to be sent as soon as possible
*/
public void updateVariable(String paintableId, String variableName,
double newValue, boolean immediate) {
addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
'd');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param newValue
* the new value to be sent
* @param immediate
* true if the update is to be sent as soon as possible
*/
public void updateVariable(String paintableId, String variableName,
boolean newValue, boolean immediate) {
addVariableToQueue(paintableId, variableName, newValue ? "true"
: "false", immediate, 'b');
}
/**
* Sends a new value for the given paintables given variable to the server.
*
* The update is actually queued to be sent at a suitable time. If immediate
* is true, the update is sent as soon as possible. If immediate is false,
* the update will be sent along with the next immediate update.
*
*
* @param paintableId
* the id of the paintable that owns the variable
* @param variableName
* the name of the variable
* @param map
* 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 map, boolean immediate) {
final StringBuffer buf = new StringBuffer();
Iterator 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(paintableMap.getPid((VPaintable) 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 VPaintableWidget) {
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.
*
* 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(paintableMap.getPid((VPaintable) 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.
*
* Selecting correct implementation
*
*
* 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.
*
*
* Caption, icon, error messages and description
*
*
* Component can delegate management of caption, icon, error messages and
* description to parent layout. This is optional an should be decided by
* component author
*
*
* Component visibility and disabling
*
* 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) {
VPaintableWidget paintable = paintableMap.getPaintable(component);
String pid = paintableMap.getPid(paintable);
if (pid == null) {
VConsole.error("Trying to update an unregistered component: "
+ Util.getSimpleName(component));
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
paintableMap.registerEventListenersFromUIDL(pid, 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, 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
paintableMap.setOffsetSize(paintable, null);
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);
}
StringBuffer styleBuf = new StringBuffer();
final String primaryName = component.getStylePrimaryName();
styleBuf.append(primaryName);
// 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(primaryName);
styleBuf.append("-");
styleBuf.append(styles[i]);
styleBuf.append(" ");
styleBuf.append(styles[i]);
}
}
// add modified classname to Fields
if (uidl.hasAttribute("modified") && component instanceof Field) {
styleBuf.append(" ");
styleBuf.append(MODIFIED_CLASSNAME);
}
TooltipInfo tooltipInfo = paintableMap.getTooltipInfo(paintable, 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());
styleBuf.append(" ");
styleBuf.append(primaryName);
styleBuf.append(ERROR_CLASSNAME_EXT);
} else {
tooltipInfo.setErrorUidl(null);
}
// add required style to required components
if (uidl.hasAttribute("required")) {
styleBuf.append(" ");
styleBuf.append(primaryName);
styleBuf.append(REQUIRED_CLASSNAME_EXT);
}
// Styles + disabled & readonly
component.setStyleName(styleBuf.toString());
// Set captions
if (manageCaption) {
final Container parent = Util.getLayout(component);
if (parent != null) {
parent.updateCaption(paintable, uidl);
}
}
/*
* updateComponentSize need to be after caption update so caption can be
* taken into account
*/
updateComponentSize(paintable, uidl);
return false;
}
private void updateComponentSize(VPaintableWidget paintable, 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 (paintableMap.getRelativeSize(paintable) == null
&& paintableMap.getOffsetSize(paintable) != null) {
// The component has changed from absolute size to relative size
relativeSizeChanges.add(paintable);
}
paintableMap.setRelativeSize(paintable, relativeSize);
} else if (relativeHeight < 0.0 && relativeWidth < 0.0) {
if (paintableMap.getRelativeSize(paintable) != null) {
// The component has changed from relative size to absolute size
relativeSizeChanges.add(paintable);
}
paintableMap.setRelativeSize(paintable, null);
}
Widget component = paintableMap.getWidget(paintable);
// 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(paintable);
}
}
/**
* 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 set = new HashSet();
for (VPaintable paintable : paintableMap.getPaintables()) {
if (paintable instanceof VPaintableWidget) {
set.add(((VPaintableWidget) paintable).getWidgetForPaintable());
}
}
Util.componentSizeUpdated(set);
}
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 (child instanceof VPaintableWidget) {
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(VPaintableWidget paintable) {
if (paintable == null) {
return false;
}
boolean debugSizes = false;
FloatSize relativeSize = paintableMap.getRelativeSize(paintable);
if (relativeSize == null) {
return false;
}
Widget widget = paintableMap.getWidget(paintable);
boolean horizontalScrollBar = false;
boolean verticalScrollBar = false;
Container parentPaintable = Util.getLayout(widget);
RenderSpace renderSpace;
// Parent-less components (like sub-windows) are relative to browser
// window.
if (parentPaintable == null) {
renderSpace = new RenderSpace(Window.getClientWidth(),
Window.getClientHeight());
} else {
renderSpace = parentPaintable.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(paintable);
}
height = (int) (height * relativeSize.getHeight() / 100.0);
if (height < 0) {
height = 0;
}
if (debugSizes) {
VConsole.log("Widget "
+ Util.getSimpleName(widget)
+ "/"
+ paintableMap.getPid(paintable)
+ " relative height "
+ relativeSize.getHeight()
+ "% of "
+ renderSpace.getHeight()
+ "px (reported by "
+ Util.getSimpleName(parentPaintable)
+ "/"
+ (parentPaintable == null ? "?" : parentPaintable
.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(paintable);
}
width = (int) (width * relativeSize.getWidth() / 100.0);
if (width < 0) {
width = 0;
}
if (debugSizes) {
VConsole.log("Widget "
+ Util.getSimpleName(widget)
+ "/"
+ paintableMap.getPid(paintable)
+ " relative width "
+ relativeSize.getWidth()
+ "% of "
+ renderSpace.getWidth()
+ "px (reported by "
+ Util.getSimpleName(parentPaintable)
+ "/"
+ (parentPaintable == null ? "?" : paintableMap
.getPid(parentPaintable)) + ") : " + 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 widget) {
return handleComponentRelativeSize(paintableMap.getPaintable(widget));
}
/**
* 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 paintableMap.getRelativeSize(paintableMap.getPaintable(widget));
}
/**
* 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 VPaintableWidget getPaintable(UIDL uidl) {
final String pid = uidl.getId();
if (!paintableMap.hasPaintable(pid)) {
// Create and register a new paintable if no old was found
VPaintableWidget p = widgetSet.createWidget(uidl, configuration);
paintableMap.registerPaintable(pid, p);
}
return (VPaintableWidget) paintableMap.getPaintable(pid);
}
/**
* 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(VPaintableWidget titleOwner,
Object key) {
if (null == titleOwner) {
return null;
}
return paintableMap.getTooltipInfo(titleOwner, key);
}
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, VPaintableWidget 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, VPaintableWidget owner,
Object key) {
tooltip.handleTooltipEvent(event, owner, key);
}
/*
* 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;
}
};
private VPaintableMap paintableMap = new VPaintableMap();
/**
* 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);
}
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, 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.
*
* Component must also pipe events to
* {@link #handleTooltipEvent(Event, VPaintableWidget, Object)} method.
*
* This method can also be used to deregister tooltips by using null as
* tooltip
*
* @param paintable
* Paintable "owning" this tooltip
* @param key
* key assosiated with given tooltip. Can be any object. For
* example a related dom element. Same key must be given for
* {@link #handleTooltipEvent(Event, VPaintableWidget, Object)}
* method.
*
* @param tooltip
* the TooltipInfo object containing details shown in tooltip,
* null if deregistering tooltip
*/
public void registerTooltip(VPaintableWidget paintable, Object key,
TooltipInfo tooltip) {
paintableMap.registerTooltip(paintable, 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(VPaintable paintable,
String eventIdentifier) {
return paintableMap.hasEventListeners(paintable, 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;
}
VPaintableMap getPaintableMap() {
return paintableMap;
}
@Deprecated
public void unregisterPaintable(VPaintable p) {
paintableMap.unregisterPaintable(p);
}
}