com.vaadin.terminal.gwt.client.ApplicationConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.terminal.gwt.client.ApplicationConnection.java

Source

/*
@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;
    }

}