Java tutorial
/* @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 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"; /** * 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<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; 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<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; 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 { // 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); } 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); } 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(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); } 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("<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() { 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<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(); 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. * <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); } // 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); } } // 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. * <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); } 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 = 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()); 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) component, uidl); } } /* * updateComponentSize need to be after caption update so caption can be * taken into account */ updateComponentSize(componentDetail, uidl); return false; } 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); } /* * 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); } 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; } }