All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nextapp.echo.webcontainer.resource.RemoteClient.js Maven / Gradle / Ivy

The newest version!
/**
 * @fileoverview
 * Remote Client Implementation.
 * 
 * Requires: Core, Serial, Core.Web, Application, Render.
 */

/**
 * A client which provides a remote view of an Echo application being executed on the server.
 * This client exchanges data with the remote server in the form of XML messages containing
 * serialized components and events.
 *        
 * Component synchronization peers that will be working exclusively with a RemoteClient may 
 * implement an optional storeProperty(clientMessage, property) method to 
 * provide custom property XML serialization.  This should be avoided if possible, but may
 * be necessary for serializing certain information such as the state of a model.
 */
Echo.RemoteClient = Core.extend(Echo.Client, {

    $static: {
        
        /**
         * Base URL from which libraries should be retrieved.
         * Libraries are loaded into global scope, and are thus not
         * bound to any particular client instance.
         * @type String
         */
        libraryServerUrl: null,
        
        /**
         * Flag indicating whether global remote client initialization has been performed.
         * @type Boolean 
         */
        initialized: false,
        
        /**
         * Default client configuration data.
         */
        DEFAULT_CONFIGURATION: {
            "NetworkError.Message": "A network error has occurred, please try again.",
            "SessionExpiration.Message": "Your session has expired.",
            "Resync.Message": "This window was not synchronized with the server and has been reset.  " + 
                    "Please try your last request again."
        },
        
        /**
         * Initializes the remote-client-based application.
         */
        init: function() {
            if (Echo.RemoteClient.initialized) {
                return;
            }
            Echo.RemoteClient.initialized = true;
            Core.Web.init();
            if (Core.Web.Env.ENGINE_MSHTML) {
                // Set documentElement.style.overflow to hidden in order to hide root scrollbar in IE.
                // This is a non-standard CSS property.
                document.documentElement.style.overflow = "hidden";
            }
        }
    },
    
    /**
     * Server user instance identifier.
     * @type String
     */
    _uiid: null,
    
    /**
     * The base server URL.
     * @type String
     */
    _serverUrl: null,
    
    /**
     * Flag indicating whether a client-server synchronization is requested.
     * @type Boolean
     */
    _syncRequested: false,

    /**
     * Flag indicating whether a client-server transaction is currently in progress.
     * @type Boolean
     */
    _transactionInProgress: false,
    
    /**
     * Identifier for input restriction registered with client during transactions.
     * @type String
     */
    _inputRestrictionId: null,
    
    /**
     * Function wrapper to invoke _processClientEvent() method.
     * @type Function
     */
    _processClientEventRef: null,
    
    /**
     * Associative array mapping between shorthand URL codes and replacement values.
     */
    _urlMappings: null,
    
    /**
     * Queue of commands to be executed.  Each command occupies two
     * indices, first index is the command peer, second is the command data.
     * @type Array
     */
    _commandQueue: null,
    
    /**
     * Outgoing client message.
     * @type Echo.RemoteClient.ClientMessage
     */
    _clientMessage: null,
    
    /**
     * AsyncManager instance which will invoke server-pushed operations.
     * @type Echo.RemoteClient.HTTPAsyncManager
     */
    _asyncManager: null,
    
    /**
     * Flag indicating whether the remote client has been initialized.
     * @type Boolean
     */
    _initialized: false,
    
    /**
     * The last component which was focused on the client before synchronization.
     * @type Component
     */
    _clientFocusedComponent: null,
    
    /**
     * The component which the server has requested to be focused.
     * @type Component
     */
    _serverFocusedComponent: null,
    
    /**
     * Current client/server transaction id.
     * @type Number
     */
    transactionId: 0,

    /**
     * Events waiting for process.
     */
    _pending_events: null,

    _initialSyncComplete: false,

    /**
     * MethodRunnable that handle client async property updates.
     */
    _asyncUpdatesHandler: null,

    /**
     * Creates a new RemoteClient instance.
     * 
     * @param serverUrl the URL of the server
     * @param initId the initialization id, which should be provided in the initial client message
     */
    $construct: function(serverUrl, initId) {
        Echo.RemoteClient.init();
        Echo.Client.call(this);

        // Install default configuration.
        for (var x in Echo.RemoteClient.DEFAULT_CONFIGURATION) {
            this.configuration[x] = Echo.RemoteClient.DEFAULT_CONFIGURATION[x];    
        }
        this._serverUrl = serverUrl;
        
        this._processClientEventRef = Core.method(this, this._processClientEvent);
        this._commandQueue = null;
        this._clientMessage = new Echo.RemoteClient.ClientMessage(this, initId);
        this._pending_events = [];
        this._asyncUpdatesHandler = new Core.Web.Scheduler.MethodRunnable(Core.method(this, this._performAsyncUpdates), 250, false);
    },
    
    /**
     * Adds a listener for an arbitrary event type to a component.
     * This method is invoked by the Serial module when event tags are
     * processed during the deserialization of component synchronization
     * XML messages.
     * 
     * @param {Echo.Component} component the component on which the listener should be added
     * @param {String} eventType the type of event
     */
    addComponentListener: function(component, eventType) {
        component.addListener(eventType, this._processClientEventRef);
    },
    
    /**
     * Decompresses a shorthand URL into a valid full-length URL.
     * A shorthand URL is expressed as "!A!xxxx" where
     * "A" is a key whose value contains the first portion of the URL
     * and "xxxx" is the latter portion of the URL.  Such URLs are used
     * to reduce the amount of bandwidth used in transmitting large
     * numbers of the same URL from server-to-client.
     *
     * @param url the shorthand URL to process
     * @return the full-length valid URL
     * @type String
     */
    decompressUrl: function(url) {
        var urlTokens = url.split("!");
        if (urlTokens[0]) {
            // urlTokens[0] is not empty: URL is not a shorthand URL.
            return url;
        } else {
            // urlTokens[0] = empty
            // urlTokens[1] = key
            // urlTokens[2] = baseUrl
            if (urlTokens.length != 3) {
                throw new Error("Invalid encoded URL: " + url);
            }
            var replacementValue = this._urlMappings[urlTokens[1]]; 
            if (replacementValue == null) {
                throw new Error("Invalid URL shorthand key \"" + urlTokens[1] + "\" in URL: " + url);
            }
            return replacementValue + urlTokens[2];
        }
    },
    
    /**
     * Test a given event, if it is a async event.
     * An async-event starts by convention with 'async_' on property eventType.
     *
     * @param eventType the string-identifier of the event
     * @return true, if it is a async event, false if not
     * @type Boolean
     */
    _isAsyncEvent: function(eventType) {
        if(eventType && typeof eventType === "string") {
            // if it startsWith 'async_' return true ->
            if(eventType.length > 5) {
                return (eventType.substring(0,5) === 'async_');
            }
        }
        // if eventType is not a string or even null:
        return false;
    },

    /**
     * Enqueues a command to be processed after component synchronization has been completed.
     * 
     * @param commandPeer the command peer to execute
     * @param commandData an object containing the command data sent from the server
     */
    _enqueueCommand: function(commandPeer, commandData) {
        if (this._commandQueue == null) {
            this._commandQueue = [];
        }
        this._commandQueue.push(commandPeer, commandData);
    },
    
    /**
     * Executes all enqueued commands; empties the queue.
     */
    _executeCommands: function() {
        if (this._commandQueue) {
            for (var i = 0; i < this._commandQueue.length; i += 2) {
                this._commandQueue[i].execute(this, this._commandQueue[i + 1]);
            }
            this._commandQueue = null;
        }
    },
    
    /**
     * Returns the URL of a library service based on the serviceId.
     * 
     * @param {String} serviceId the serviceId
     * @return the full library URL
     * @type String
     */
    _getLibraryServiceUrl: function(serviceId) {
        if (!Echo.RemoteClient._libraryServerUrl) {
            Echo.RemoteClient._libraryServerUrl = this._serverUrl;
        }
        if (this._uiid == null) {
            return Echo.RemoteClient._libraryServerUrl + "?sid=" + serviceId;
        } else {
            return Echo.RemoteClient._libraryServerUrl + "?sid=" + serviceId + "&uiid=" + this._uiid;
        }
    },
    
    /** @see Echo.Client#getResoruceUrl */
    getResourceUrl: function(packageName, resourceName) {
        return this.getServiceUrl("Echo.Resource") + "&pkg=" + packageName + "&res=" + resourceName;
    },
    
    /**
     * Returns the URL of a remote server-side service.
     *
     * @param {String} serviceId the service id
     * @return the URL
     * @type String
     */
    getServiceUrl: function(serviceId) {
        if (this._uiid == null) {
            return this._serverUrl + "?sid=" + serviceId;
        } else {
            return this._serverUrl + "?sid=" + serviceId + "&uiid=" + this._uiid;
        }
    },

    /**
     * Handles an invalid response from the server.
     * 
     * @param e the HttpConnection response event
     */
    _handleInvalidResponse: function(e) {
        var detail = null;
        if (e.exception) {
            detail = e.exception.toString();
        } else if (e.source.getResponseText()) {
            if (e.source.getResponseText().indexOf("!*! Session Expired") != -1) {
                this._handleSessionExpiration();
                return;
            } else {
                if (this.configuration["InvalidResponse.Restart"]) {
                    window.location.reload();
                    return;
                } else if (this.configuration["InvalidResponse.URI"]) {
                    window.location.href = this.configuration["InvalidResponse.URI"];
                    return;
                }
                detail = e.source.getResponseText();
            }
        } else {
            this._handleNetworkError();
            return;
        }
        this.fail(detail);
    },
    
    /**
     * Handles server-side session expiration.
     */
    _handleSessionExpiration: function() {
        var element = this.domainElement;
        try {
            this.dispose();
        } finally {
            if (this.configuration["SessionExpiration.Restart"]) {
                // Restart immediately.
                window.location.reload();
            } else if (this.configuration["SessionExpiration.URI"]) {
                // Redirect.
                window.location.href = this.configuration["SessionExpiration.URI"];
            } else {
                // Display error.
                this.displayError(element, this.configuration["SessionExpiration.Message"], null, 
                        this.configuration["Action.Continue"], function() {
                    window.location.reload();
                }, Echo.Client.STYLE_MESSAGE);
            }
        }
    },
    
    _handleNetworkError: function() {
        var element = this.domainElement;
        try {
            this.dispose();
        } finally {
            // Display error.
            this.displayError(element, this.configuration["NetworkError.Message"], null, 
                    this.configuration["Action.Continue"], function() {
                window.location.reload();
            }, Echo.Client.STYLE_MESSAGE);
        }
    },
    
    /**
     * Initializes the remote client.  This method will perform the following operations:
     * 
    *
  • Find the domain element in which the application should exist by parsing the * initial server message XML document.
  • *
  • Create a new Echo.Application instance,
  • *
  • Register a component update listener on that application instance such that * a user's input will be stored in the outgoing ClientMessage.
  • *
  • Invoke Echo.Client.configure() to initialize the client.
  • *
* * @param {Document} initialResponseDocument the initial ServerMessage XML document * received from the server (this document contains some necessary start-up information * such as the id of the root element) */ init: function(initialResponseDocument) { this._uiid = initialResponseDocument.documentElement.getAttribute("u"); if (this._uiid === "") { this._uiid = null; } this._urlMappings = {}; this._urlMappings.I = this._serverUrl + "?" + (this._uiid == null ? "" : "uiid=" + this._uiid + "&") + "sid=Echo.Image&iid="; // Find domain element. var domainElementId = initialResponseDocument.documentElement.getAttribute("root"); var domainElement = document.getElementById(domainElementId); if (!domainElement) { throw new Error("Cannot find domain element: " + domainElementId); } // Create an application instance. var application = new Echo.Application(); // Register an update listener to receive notification of user actions such that they // may be remarked in the outgoing ClientMessage. application.addListener("componentUpdate", Core.method(this, this._processClientUpdate)); // Perform general purpose client configuration. this.configure(application, domainElement); // Mark the client as initialized. this._initialized = true; }, _processPrendingClientEvent: function(e) { this._clientMessage.setEvent(e.source.renderId, e.type, e.data); this._syncRequested = true; Core.Web.Scheduler.run(Core.method(this, this.sync)); // added: && !this._isAsyncEvent(e.type) to handle async events w/o input restriction if (!this._inputRestrictionId && !this._isAsyncEvent(e.type)) { this._inputRestrictionId = this.createInputRestriction(); } }, /** * Processes an event from a component that requires immediate server interaction. * * @param e the event to process */ _processClientEvent: function(e) { if (this._syncRequested || this._transactionInProgress || this._pending_events.length > 0) { this._pending_events.push( e ); return; } this._processPrendingClientEvent(e); }, /** * Processes a user update to a component (storing the updated state in the outgoing * client message). * * @param e the property update event from the component */ _processClientUpdate: function(e) { if (this._processServerMessage) { //ignore updates that are from serverMessage } else { //Store property update from the component. (by peer or clientMessage) if (e.parent.peer && e.parent.peer.storeProperty && e.parent.peer.storeProperty(this._clientMessage, e.propertyName)) { //property is stored by peer } else { this._clientMessage.storeProperty(e.parent.renderId, e.propertyName, e.newValue); } if (!this._transactionInProgress) { // has an asynchronous update (e.g: timer periodically set a property) Core.Web.Scheduler.update(this._asyncUpdatesHandler, true); } } }, /** * Invoked when there is asynchronous property updates. */ _performAsyncUpdates: function() { if (!this._transactionInProgress && !this._syncRequested && this._clientMessage.hasStoredProperties()) { this._syncRequested = true; this.sync(); } }, /** * ServerMessage completion listener. * * @param e the server message completion event */ _processSyncComplete: function(e) { this._processServerMessage = false; // Mark time of serialization completion with profiling timer. if (Echo.Client.profilingTimer) { Echo.Client.profilingTimer.mark("ser"); } this.processUpdates(); this._executeCommands(); // Flag transaction as being complete. this._transactionInProgress = false; this.removeInputRestriction(this._inputRestrictionId); this._inputRestrictionId = null; // Focus component if (this._serverFocusedComponent) { this.application.setFocusedComponent(this._serverFocusedComponent); } if (Echo.Client.profilingTimer) { Core.Debug.consoleWrite(Echo.Client.profilingTimer + " /pc:" + Echo.Render._loadedPeerCount); Echo.Client.profilingTimer = null; } if (e.source.resync) { this.displayError(this.domainElement, this.configuration["Resync.Message"], null, this.configuration["Action.Continue"], null, Echo.Client.STYLE_MESSAGE); } if (this._pending_events.length > 0) { // if have pending events no need to check for "subupdates" this._processPrendingClientEvent(this._pending_events.shift()); } else if (this.application.updateManager.hasUpdates()) { // current updates have created a new updates // make a new synchronization this._syncRequested = true; Core.Web.Scheduler.run(Core.method(this, this.sync)); } if (this._asyncManager && this._asyncManager._syncComplete) { this._asyncManager._syncComplete(); } if (!this._initialSyncComplete) { if (this._isBrowserOutdated()) { this._showBrowserWarning(); } this._initialSyncComplete = true; } }, /** * Process a response to a client-server synchronization. * * @param e the HttpConnection response event */ _processSyncResponse: function(e) { // Retrieve response document. var responseDocument = e.source.getResponseXml(); // Verify that response document exists and is valid. if (!e.valid || !responseDocument || !responseDocument.documentElement) { this._handleInvalidResponse(e); return; } // If this is the first ServerMessage received, initialize the client // This step will create the application, determine where in the DOM the application should be // rendered, and so forth. if (!this._initialized) { this.init(responseDocument); } // Profiling Timer (Un-comment to enable, comment to disable). Echo.Client.profilingTimer = new Echo.Client.Timer(); // Create new ServerMessage object with response document. var serverMessage = new Echo.RemoteClient.ServerMessage(this, responseDocument); this.transactionId = serverMessage.transactionId; // Add completion listener to invoke _processSyncComplete when message has been fully processed. // (Some elements of the server message are processed asynchronously). serverMessage.addCompletionListener(Core.method(this, this._processSyncComplete)); // Start server message processing. this._processServerMessage = true; serverMessage.process(); }, /** * Removes a listener for an arbitrary event type from a component. * * @param {Echo.Component} component the component from which the listener should be removed * @param {String} eventType the type of event */ removeComponentListener: function(component, eventType) { component.removeListener(eventType, this._processClientEventRef); }, /** * Initiates a client-server synchronization. */ sync: function() { if (this._transactionInProgress) { throw new Error("Attempt to invoke client/server synchronization while another transaction is in progress; " + "event data: componentId=" + this._clientMessage._eventComponentId + " eventType=" + this._clientMessage._eventType + " eventData=" + this._clientMessage._eventData); } this._clientFocusedComponent = this.application ? this.application.getFocusedComponent() : null; this._serverFocusedComponent = null; this._transactionInProgress = true; this._syncRequested = false; // added: && !this._isAsyncEvent(this._clientMessage._eventType) to handle async events if (!this._inputRestrictionId && !this._isAsyncEvent(this._clientMessage._eventType)) { this._inputRestrictionId = this.createInputRestriction(); } if (this._asyncManager) { this._asyncManager._stop(); } this._syncInitTime = new Date().getTime(); var conn = new Core.Web.HttpConnection(this.getServiceUrl("Echo.Sync"), "POST", this._clientMessage._renderXml(), "text/xml;charset=utf-8"); // Create new client message. this._clientMessage = new Echo.RemoteClient.ClientMessage(this, null); conn.addResponseListener(Core.method(this, this._processSyncResponse)); conn.connect(); } }); /** * Abstract base class for server message directive processors. */ Echo.RemoteClient.DirectiveProcessor = Core.extend({ /** * The remote client instance. * @type Echo.RemoteClient */ client: null, /** * Constructor. * @param {Echo.RemoteClient} client the remote client instance */ $construct: function(client) { this.client = client; }, $abstract: { /** * Process a server message directive. * * @param {Element} dirElement the directive element */ process: function(dirElement) { } } }); /** * Manages server-pushed updates to the client. */ Echo.RemoteClient.AbstractAsyncManager = Core.extend({ $static: { /** * Number of times the client should attempt to contact the server. * * @type Number */ MAX_CONNECT_ATTEMPTS: 3 }, /** * The supported client. * * @type Echo.RemoteClient */ _client: null, /** * The number of times the client has unsuccessfully attempted to contact the server. * * @type Number */ _failedConnectAttempts: null, /** * Creates a new asynchronous manager. * * @param {Echo.RemoteClient} client the supported client */ $construct: function(client) { this._client = client; }, $abstract: true, $virtual: { /** * Starts server polling for asynchronous tasks. */ _start: function() { }, /** * Stops server polling for asynchronous tasks. */ _stop: function() { } } }); Echo.RemoteClient.WebSocketAsyncManager = Core.extend(Echo.RemoteClient.AbstractAsyncManager, { $static: { /** * Message from server when requaried synchronization. */ RQ_SYNC_MESSAGE: "request-sync", /** * Close code when the user opens a new socket. (e.g: new browser tab). */ SYNC_CLOSE_CODE: 8807, /** * Close code when application instance is disposed. */ DISPOSE_CLOSE_CODE: 8806 }, /** * Socket that listens for messages from the server. * * @type Core.Web.WebSocketConnection */ _wsConnection: null, /** * Function wrapper to invoke _onEvents() method. * @type Function */ _eventsHandler: null, /** * Indicates that is sync successfully. */ _unsync: null, /** * Creates a new asynchronous manager basedon Core.Web.WebSocketConnection. * * @param {Echo.RemoteClient} client the supported client */ $construct: function(client) { Echo.RemoteClient.AbstractAsyncManager.call(this, client); var loc = window.location; var protocol = loc.protocol == "http:" ? "ws:" : loc.protocol == "https:" ? "wss:" : null; if (!protocol) { throw new Error("Echo.RemoteClient.WebSocketAsyncManager: unknown protocol!"); } var URL = protocol + "//" + loc.hostname + ":" + loc.port + loc.pathname + "ws"; if (this._client.uiid) { URL += "?uiid=" + this._client._uiid; } this._wsConnection = new Core.Web.WebSocketConnection(URL); this._eventsHandler = Core.method(this, this._onEvents); }, /** * Open web socket connection. * Register for the required events. */ _start: function() { var state = this._wsConnection.getState(); if (state == Core.Web.WebSocketConnection.STATE_DISPOSED) { this._wsConnection.addEventListener(Core.Web.WebSocketConnection.EVENT_OPEN, this._eventsHandler); this._wsConnection.addEventListener(Core.Web.WebSocketConnection.EVENT_CLOSE, this._eventsHandler); this._wsConnection.addEventListener(Core.Web.WebSocketConnection.EVENT_MESSAGE, this._eventsHandler); this._wsConnection.addEventListener(Core.Web.WebSocketConnection.EVENT_ERROR, this._eventsHandler); } this._wsConnection.open(); }, /** * Close current web socket connection. */ _stop: function(real) { if (real) { this._wsConnection.close(); } }, /** * Call is when an event occurs. * * For various events taking different approaches. */ _onEvents: function(e) { switch(e.type) { case Core.Web.WebSocketConnection.EVENT_OPEN: this._failedConnectAttempts = 0; if (!this._executeSync()) { this.unsync = true; } break; case Core.Web.WebSocketConnection.EVENT_MESSAGE: if (e.data.data == Echo.RemoteClient.WebSocketAsyncManager.RQ_SYNC_MESSAGE) { if (!this._executeSync()) { this._unsync = true; } } break; case Core.Web.WebSocketConnection.EVENT_CLOSE: if (e.data.code == Echo.RemoteClient.WebSocketAsyncManager.DISPOSE_CLOSE_CODE) { this._client.fail(e.data.reason); } else if (e.data.code == Echo.RemoteClient.WebSocketAsyncManager.SYNC_CLOSE_CODE || ++this._failedConnectAttempts >= Echo.RemoteClient.AbstractAsyncManager.MAX_CONNECT_ATTEMPTS) { this._notifyForNetworkError(); } else { this._start(); } break; case Core.Web.WebSocketConnection.EVENT_ERROR: this._notifyForNetworkError(); break; } }, /** * Call when synchronization finished. * If is unsync, start again. */ _syncComplete: function() { if (this._unsync && this._executeSync()) { this._unsync = false; } }, /** * Make sync in new thread. * * @return {Boolean} whether the sync method is started. */ _executeSync: function() { if (!this._client._transactionInProgress && !this._client._syncRequested) { this._client._syncRequested = true; this._client.sync(); return true; } else { return false; } }, /** * Notify client for network errors. * Close WebScoket. */ _notifyForNetworkError: function() { Core.Web.Scheduler.run(Core.method(this, function() { this._stop(true); })); this._client._handleNetworkError(); } }); Echo.RemoteClient.HTTPAsyncManager = Core.extend(Echo.RemoteClient.AbstractAsyncManager, { /** * The repeating runnable used for server polling. * * @type Core.Web.Scheduler.Runnable */ _runnable: null, /** * Creates a new asynchronous manager basedon Core.Web.HttpConnection. * * @param {Echo.RemoteClient} client the supported client */ $construct: function(client) { Echo.RemoteClient.AbstractAsyncManager.call(this, client); this._runnable = new Core.Web.Scheduler.MethodRunnable(Core.method(this, this._pollServerForUpdates), 1000, false); }, /** * Creates and invokes a new HttpConnection to the server to poll the server and determine whether * it has any updates that need to be pushed to the client. */ _pollServerForUpdates: function() { var conn = new Core.Web.HttpConnection(this._client.getServiceUrl("Echo.AsyncMonitor"), "GET"); conn.addResponseListener(Core.method(this, this._processPollResponse)); conn.connect(); }, /** * Response processor for server polling request. * In the event a server action is required, this method will submit the client message to the * server immediately. The server will push any updates into the reciprocated server message. * If no action is required, the next polling interval will be scheduled. * * @param e the poll response event */ _processPollResponse: function(e) { var responseDocument = e.source.getResponseXml(); if (e.valid && responseDocument && responseDocument.documentElement) { this._failedConnectAttempts = 0; if (responseDocument.documentElement.getAttribute("request-sync") == "true") { if (!this._client._transactionInProgress && !this._client._syncRequested) { this._client.sync(); } return; } } else if (++this._failedConnectAttempts >= Echo.RemoteClient.AbstractAsyncManager.MAX_CONNECT_ATTEMPTS) { this._client._handleInvalidResponse(e); this._failedConnectAttempts = 0; return; } Core.Web.Scheduler.add(this._runnable); }, /** * Sets the interval at which the server should be polled. * * @param {Number} interval the new polling interval, in milliseconds */ _setInterval: function(interval) { this._runnable.timeInterval = interval; }, /** * Starts server polling for asynchronous tasks. */ _start: function() { this._failedConnectAttempts = 0; Core.Web.Scheduler.add(this._runnable); }, /** * Stops server polling for asynchronous tasks. */ _stop: function() { Core.Web.Scheduler.remove(this._runnable); } }); /** * Client-to-server synchronization message. * This object is used to collect state changes on the client and then * render an XML version to be POSTed to a server-side Echo application. */ Echo.RemoteClient.ClientMessage = Core.extend({ $static: { /** * @class Utility class for constructing the client properties directive. */ _ClientProperties: Core.extend({ /** * The directive element * @type Element */ _element: null, /** * The client message on which the client properties directive will be sent. * @type Echo.RemoteClient.ClientMessage */ _clientMessage: null, /** * Creates a new ClientProperties directive object. * * @param {Echo.RemoteClient.ClientMessage} clientMessage the client message object * @param map initial properties to store (key/value pairs) */ $construct: function(clientMessage, map) { this._element = clientMessage._document.createElement("dir"); this._element.setAttribute("proc", "ClientProperties"); clientMessage._document.documentElement.appendChild(this._element); this._clientMessage = clientMessage; if (map) { for (var key in map) { this._add(key, map[key]); } } }, /** * Constructs a property element with the given key-value pair and adds that to the * client properties directive in the client message. * * @param {String} key the key * @param value the value */ _add: function(key, value) { if (value == null) { return; } var element = this._clientMessage._document.createElement("p"); element.setAttribute("n", key); Echo.Serial.storeProperty(this._clientMessage._client, element, value); this._element.appendChild(element); } }) }, /** * The RemoteClient which generated this message. * @type Echo.RemoteClient */ _client: null, /** * Mapping between component ids and updated property values. * Values in this map are updated by the storeProperty() method. * These values will be rendered to XML when required. */ _componentIdToPropertyMap: null, /** * Id of the component which fired the event that is responsible for * the client message being sent to the server. * @type String */ _eventComponentId: null, /** * Type of event fired to cause server interaction. * @type String */ _eventType: null, /** * Event data object of event responsible for server interaction. */ _eventData: null, /** * The DOM object to which the client message will be rendered. * @type Document */ _document: null, /** * Creates a new client message. * * @param {Echo.RemoteClient} client the RemoteClient * @param {String} initId the initialization id provided to the RemoteClient constructor (if this an initialization request) * or null if it is not */ $construct: function(client, initId) { this._client = client; this._componentIdToPropertyMap = {}; this._document = Core.Web.DOM.createDocument("http://www.nextapp.com/products/echo/svrmsg/clientmessage.3.0", "cmsg"); if (initId != null) { this._document.documentElement.setAttribute("t", "init"); this._document.documentElement.setAttribute("w", Echo.Client.windowId); this._document.documentElement.setAttribute("ii", initId); this._renderClientProperties(); } }, /** * Queries the application for the currently focused component and renders * this information to the client message DOM. */ _renderCFocus: function() { if (!this._client.application) { return; } var focusedComponent = this._client.application.getFocusedComponent(); if (focusedComponent && focusedComponent.renderId.substring(0,2) == "C.") { var cFocusElement = this._document.createElement("dir"); cFocusElement.setAttribute("proc", "CFocus"); var focusElement = this._document.createElement("focus"); focusElement.setAttribute("i", focusedComponent.renderId); cFocusElement.appendChild(focusElement); this._document.documentElement.appendChild(cFocusElement); } }, /** * Renders component hierarchy state change information to the client message DOM. * This information is retrieved from instance variables of the client message object, * i.e., the component-id-to-property-value map and event properties. */ _renderCSync: function() { var cSyncElement = this._document.createElement("dir"); cSyncElement.setAttribute("proc", "CSync"); // Render event information. if (this._eventType) { var eElement = this._document.createElement("e"); eElement.setAttribute("t", this._eventType); eElement.setAttribute("i", this._eventComponentId); if (this._eventData != null) { Echo.Serial.storeProperty(this._client, eElement, this._eventData); } cSyncElement.appendChild(eElement); } // Render property information. for (var componentId in this._componentIdToPropertyMap) { var propertyMap = this._componentIdToPropertyMap[componentId]; var component = this._client.application.getComponentByRenderId(componentId); for (var propertyName in propertyMap) { var propertyValue = propertyMap[propertyName]; var pElement = this._document.createElement("p"); pElement.setAttribute("i", componentId); pElement.setAttribute("n", propertyName); Echo.Serial.storeProperty(this._client, pElement, propertyValue); cSyncElement.appendChild(pElement); } } this._document.documentElement.appendChild(cSyncElement); }, /** * Renders information about the client environment to the client message DOM. * This information is rendered only in the first client message to the server. */ _renderClientProperties: function() { var env = Core.Web.Env; var cp = new Echo.RemoteClient.ClientMessage._ClientProperties(this, { screenWidth: screen.width, screenHeight: screen.height, screenColorDepth: screen.colorDepth, utcOffset: 0 - parseInt(new Date().getTimezoneOffset(), 10), navigatorAppName: window.navigator.appName, navigatorAppVersion: window.navigator.appVersion, navigatorAppCodeName: window.navigator.appCodeName, navigatorCookieEnabled: window.navigator.cookieEnabled, navigatorJavaEnabled: window.navigator.javaEnabled(), navigatorLanguage: window.navigator.language ? window.navigator.language : window.navigator.userLanguage, navigatorPlatform: window.navigator.platform, navigatorUserAgent: window.navigator.userAgent, browserChrome: env.BROWSER_CHROME, browserOpera: env.BROWSER_OPERA, browserSafari: env.BROWSER_SAFARI, browserKonqueror: env.BROWSER_KONQUEROR, browserMozillaFirefox: env.BROWSER_FIREFOX, browserMozilla: env.BROWSER_MOZILLA, browserInternetExplorer: env.BROWSER_INTERNET_EXPLORER, browserVersionMajor: env.BROWSER_VERSION_MAJOR, browserVersionMinor: env.BROWSER_VERSION_MINOR, engineGecko: env.ENGINE_GECKO, engineKHTML: env.ENGINE_KHTML, engineMSHTML: env.ENGINE_MSHTML, enginePresto: env.ENGINE_PRESTO, engineWebKit: env.ENGINE_WEBKIT, engineVersionMajor: env.ENGINE_VERSION_MAJOR, engineVersionMinor: env.ENGINE_VERSION_MINOR }); }, /** * Renders all information to the XML DOM and returns it. * * @return the DOM * @type Document */ _renderXml: function() { if (!this._rendered) { this._renderCFocus(); this._renderCSync(); this._document.documentElement.setAttribute("i", this._client.transactionId); this._rendered = true; } return this._document; }, /** * Sets the event that will cause the client-server interaction. * * @param {String} componentId the renderId of the event-firing component * @param {String} eventType the type of the event * @param the event data object */ setEvent: function(componentId, eventType, eventData) { if (this._eventComponentId) { return; } this._eventComponentId = componentId; this._eventType = eventType; this._eventData = eventData; }, /** * Stores information about a property change to a component. * * @param {String} componentId the renderId of the component * @param {String} propertyName the name of the property * @param the new property value */ storeProperty: function(componentId, propertyName, propertyValue) { var propertyMap = this._componentIdToPropertyMap[componentId]; if (!propertyMap) { propertyMap = {}; this._componentIdToPropertyMap[componentId] = propertyMap; } propertyMap[propertyName] = propertyValue; }, hasStoredProperties: function() { for(var property in this._componentIdToPropertyMap) { if(this._componentIdToPropertyMap.hasOwnProperty(property)) { return true; } } return false; } }); /** * Namespace/base class for command execution peers. * @namespace */ Echo.RemoteClient.CommandExec = Core.extend({ $abstract: true, $static: { /** * Executes the command. * Command implementations should override. Not inherited, provided for documentation purposes. * * @param {Echo.Client} client the client * @param commandData object containing command properties provided from server */ execute: function(client, commandData) { } } }); /** * Server message processing facility. * Parses XML DOM of message received from server, first loading required modules and then delegating to registered server message * directive processors. */ Echo.RemoteClient.ServerMessage = Core.extend({ $static: { /** * Mapping between processor names and instantiable Echo.RemoteClient.DirectiveProcessor types. */ _processorClasses: { }, /** * Registers a server message directive processor. * * @param {String} name the processor name * @param {Echo.RemoteClient.DirectiveProcessor} the processor */ addProcessor: function(name, processor) { this._processorClasses[name] = processor; } }, /** * @type Echo.RemoteClient */ client: null, /** Provided transaction id. */ transactionId: null, /** * The server message DOM. * @type Document */ document: null, /** * Listener storage facility. * @type Core.ListenerList */ _listenerList: null, /** * Mapping between directive processor names and processor instances. */ _processorInstances: null, /** * Flag indicating whether full-resynchronization is required. */ resync: false, /** * Creates a new ServerMessage. * * @param {Echo.RemoteClient} the client * @param {Document} the received server message DOM */ $construct: function(client, xmlDocument) { this.client = client; this.document = xmlDocument; this._listenerList = new Core.ListenerList(); this._processorInstances = { }; this.transactionId = xmlDocument.documentElement.getAttribute("i"); if (xmlDocument.documentElement.getAttribute("resync")) { this.resync = true; } }, /** * Adds a completion listener to be notified when the server message has been processed. * * @param {Function} l the listener to add */ addCompletionListener: function(l) { this._listenerList.addListener("completion", l); }, /** * Begins processing. This method may return asynchronously, completion listeners should be registered to determine * when processing has completed. */ process: function() { // Processing phase 1: load libraries. var libsElement = Core.Web.DOM.getChildElementByTagName(this.document.documentElement, "libs"); if (libsElement) { var libraryGroup = new Core.Web.Library.Group(); var element = libsElement.firstChild; while (element) { if (element.nodeType == 1) { if (element.nodeName == "lib") { var url = this.client._getLibraryServiceUrl(element.firstChild.data); var type = element.getAttribute("type"); libraryGroup.add(url, type); } } element = element.nextSibling; } if (libraryGroup.hasNewLibraries()) { libraryGroup.addLoadListener(Core.method(this, this._processPostLibraryLoad)); libraryGroup.load(); } else { this._processPostLibraryLoad(); } } else { this._processPostLibraryLoad(); } }, /** * Performs processing operations after required JavaScript modules have been loaded. * May have been invoked directly by process() or as a Core.Web.LibraryGroup loadListener. * Notifies completion listeners of processing completion. */ _processPostLibraryLoad: function(e) { try { if (Echo.Client.profilingTimer) { Echo.Client.profilingTimer.mark("lib"); // Library Loading } if (e && !e.success) { this.client.fail("Cannot install library: " + e.url + " Exception: " + e.ex); return; } // Processing phase 2: invoke directives. var groupElements = Core.Web.DOM.getChildElementsByTagName(this.document.documentElement, "group"); for (var i = 0; i < groupElements.length; ++i) { var dirElements = Core.Web.DOM.getChildElementsByTagName(groupElements[i], "dir"); for (var j = 0; j < dirElements.length; ++j) { var procName = dirElements[j].getAttribute("proc"); var processor = this._processorInstances[procName]; if (!processor) { // Create new processor instance. if (!Echo.RemoteClient.ServerMessage._processorClasses[procName]) { throw new Error("Invalid processor specified in ServerMessage: " + procName); } processor = new Echo.RemoteClient.ServerMessage._processorClasses[procName](this.client); this._processorInstances[procName] = processor; } processor.process(dirElements[j]); } } // Complete: notify listeners of completion. this._listenerList.fireEvent({type: "completion", source: this}); // Start server push listener if required. var async_interval = parseInt(this.document.documentElement.getAttribute("async-interval"), 10); var ws_enable = this.document.documentElement.getAttribute("ws-enable") == "true"; if (async_interval) { if (Core.Web.WebSocketConnection.isAvailable() && ws_enable) { if (!this.client._asyncManager) { this.client._asyncManager = new Echo.RemoteClient.WebSocketAsyncManager(this.client); } } else { if (!this.client._asyncManager) { this.client._asyncManager = new Echo.RemoteClient.HTTPAsyncManager(this.client); } this.client._asyncManager._setInterval(async_interval); } this.client._asyncManager._start(); } } catch (ex) { this.client.fail("Exception: " + ex + " -> " + ex.stack); } }, /** * Removes a completion listener to be notified when the server message has been processed. * * @param {Function} l the listener to remove */ removeCompletionListener: function(l) { this._listenerList.removeListener("completion", l); } }); // Echo.RemoteClient.DirectiveProcessor Implementations /** * SerevrMessage directive processor for general application-related synchronization. */ Echo.RemoteClient.ApplicationSyncProcessor = Core.extend(Echo.RemoteClient.DirectiveProcessor, { $load: function() { Echo.RemoteClient.ServerMessage.addProcessor("AppSync", this); }, /** @see #Echo.RemoteClient.DirectiveProcessor#process */ process: function(dirElement) { var propertyElement = dirElement.firstChild; while (propertyElement) { if (propertyElement.nodeName == "locale") { this.client.application.setLocale(propertyElement.firstChild.nodeValue); } if (propertyElement.nodeName == "dir") { this.client.application.setLayoutDirection(propertyElement.firstChild.nodeValue === "rtl" ? Echo.LayoutDirection.RTL : Echo.LayoutDirection.LTR); } if (propertyElement.nodeName == "config") { // Install customized configuration. var pElement = propertyElement.firstChild; while (pElement) { Echo.Serial.loadProperty(this.client, pElement, null, this.client.configuration, null); pElement = pElement.nextSibling; } } propertyElement = propertyElement.nextSibling; } } }); /** * SerevrMessage directive processor for command executions. */ Echo.RemoteClient.CommandExecProcessor = Core.extend(Echo.RemoteClient.DirectiveProcessor, { $static: { /** Mapping between command type names and command peer objects */ _typeToPeerMap: {}, /** * Registers a command execution peer. * * @param {String} type the command type name * @param {Echo.RemoteClient.CommandExec} commandPeer an object providing an 'execute()' method which be invoked to * execute the command. */ registerPeer: function(type, commandPeer) { Echo.RemoteClient.CommandExecProcessor._typeToPeerMap[type] = commandPeer; } }, $load: function() { Echo.RemoteClient.ServerMessage.addProcessor("CmdExec", this); }, /** @see #Echo.RemoteClient.DirectiveProcessor#process */ process: function(dirElement) { var cmdElement = dirElement.firstChild; while (cmdElement) { var type = cmdElement.getAttribute("t"); var commandPeer = Echo.RemoteClient.CommandExecProcessor._typeToPeerMap[type]; if (!commandPeer) { throw new Error("Command peer not found for: " + type); } var commandData = {}; var pElement = cmdElement.firstChild; while (pElement) { Echo.Serial.loadProperty(this.client, pElement, null, commandData, null); pElement = pElement.nextSibling; } this.client._enqueueCommand(commandPeer, commandData); cmdElement = cmdElement.nextSibling; } } }); /** * ServerMessage directive processor for component focus. */ Echo.RemoteClient.ComponentFocusProcessor = Core.extend(Echo.RemoteClient.DirectiveProcessor, { $load: function() { Echo.RemoteClient.ServerMessage.addProcessor("CFocus", this); }, /** @see #Echo.RemoteClient.DirectiveProcessor#process */ process: function(dirElement) { var element = dirElement.firstChild; while (element) { if (element.nodeType == 1 && element.nodeName == "focus") { var newComponent = this.client.application.getComponentByRenderId(element.getAttribute("i")); if (newComponent != this.client._clientFocusedComponent) { this.client._serverFocusedComponent = newComponent; } } element = element.nextSibling; } } }); /** * ServerMessage directive processor for component synchronizations (initialization phase). * * Processes initialization directives related to component hierarchy. * Currently limited to clearing entire hierarchy. */ Echo.RemoteClient.ComponentSyncInitProcessor = Core.extend(Echo.RemoteClient.DirectiveProcessor, { $load: function() { Echo.RemoteClient.ServerMessage.addProcessor("CSyncIn", this); }, /** @see #Echo.RemoteClient.DirectiveProcessor#process */ process: function(dirElement) { var element = dirElement.firstChild; while (element) { if (element.nodeType == 1 && element.nodeName == "cl") { this.client.application.rootComponent.removeAll(); } element = element.nextSibling; } } }); /** * ServerMessage directive processor for component synchronizations (remove phase). * * Processes directives to remove components. Performed before update phase to * bring component hierarchy to a minimal state before updates occur. */ Echo.RemoteClient.ComponentSyncRemoveProcessor = Core.extend(Echo.RemoteClient.DirectiveProcessor, { $static: { /** * Reverse array sorter for removing components by index from last to first. * @see Array#sort */ _numericReverseSort: function(a, b) { return b - a; } }, $load: function() { Echo.RemoteClient.ServerMessage.addProcessor("CSyncRm", this); }, /** @see #Echo.RemoteClient.DirectiveProcessor#process */ process: function(dirElement) { var rmElement = dirElement.firstChild; while (rmElement) { if (rmElement.nodeType != 1) { continue; } // Determine parent component. var parentComponent; if (rmElement.getAttribute("r") == "true") { parentComponent = this.client.application.rootComponent; } else { var parentId = rmElement.getAttribute("i"); parentComponent = this.client.application.getComponentByRenderId(parentId); } // Retrieve child ids and remove. var childElementIds = rmElement.getAttribute("rm").split(","); this._removeComponents(parentComponent, childElementIds); rmElement = rmElement.nextSibling; } }, /** * Removes components with specified ids from specified parent component. * * @param {Echo.Component} parentComponent the parent component * @param {Array} childElementIds array containing ids of child elements to remove. */ _removeComponents: function(parentComponent, childElementIds) { var i; if (childElementIds.length > 5) { // Special case: many children being removed: create renderId -> index map and remove by index // in order to prevent Component.indexOf() of from being invoked n times. // Create map between ids and indices. var idToIndexMap = {}; for (i = 0; i < parentComponent.children.length; ++i) { idToIndexMap[parentComponent.children[i].renderId] = i; } // Create array of indices to remove. var indicesToRemove = []; for (i = 0; i < childElementIds.length; ++i) { var index = idToIndexMap[childElementIds[i]]; if (index != null) { indicesToRemove.push(parseInt(index, 10)); } } indicesToRemove.sort(Echo.RemoteClient.ComponentSyncRemoveProcessor._numericReverseSort); // Remove components (last to first). for (i = 0; i < indicesToRemove.length; ++i) { parentComponent.remove(indicesToRemove[i]); } } else { for (i = 0; i < childElementIds.length; ++i) { var component = this.client.application.getComponentByRenderId(childElementIds[i]); if (component) { parentComponent.remove(component); } } } } }); /** * ServerMessage directive processor for component synchronizations (update phase). * * Processes directives to update components (add children, update properties), * clear entire component hierarchy (for full-rerender), set stylesheet, * and store referenced properties/styles. */ Echo.RemoteClient.ComponentSyncUpdateProcessor = Core.extend(Echo.RemoteClient.DirectiveProcessor, { $load: function() { Echo.RemoteClient.ServerMessage.addProcessor("CSyncUp", this); }, /** Mapping between referenced property ids and values. */ _propertyMap : null, /** Mapping between referenced style ids and values. */ _styleMap: null, /** @see #Echo.RemoteClient.DirectiveProcessor#process */ process: function(dirElement) { var element; element = dirElement.firstChild; while (element) { if (element.nodeType == 1) { switch (element.nodeName) { case "ss": this._processStyleSheet(element); break; case "up": this._processUpdate(element); break; case "rp": this._processReferencedProperties(element); break; case "rs": this._processReferencedStyles(element); break; } } element = element.nextSibling; } }, /** * Process an "rp" directive to store referenced properties during the synchronization. * * @param {Element} rpElement the directive element */ _processReferencedProperties: function(rpElement) { var propertyElement = rpElement.firstChild; while (propertyElement) { if (propertyElement.nodeName == "p") { var propertyId = propertyElement.getAttribute("i"); var propertyType = propertyElement.getAttribute("t"); var translator = Echo.Serial.getPropertyTranslator(propertyType); if (!translator) { throw new Error("Translator not available for property type: " + propertyType); } var propertyValue = translator.toProperty(this.client, propertyElement); if (!this._propertyMap) { this._propertyMap = {}; } this._propertyMap[propertyId] = propertyValue; } propertyElement = propertyElement.nextSibling; } }, /** * Process an "rs" directive to store referenced styles during the synchronization. * * @param {Element} rsElement the directive element */ _processReferencedStyles: function(rsElement) { var styleElement = rsElement.firstChild; while (styleElement) { if (styleElement.nodeName == "s") { var styleId = styleElement.getAttribute("i"); var style = { }; var propertyElement = styleElement.firstChild; while (propertyElement) { Echo.Serial.loadProperty(this.client, propertyElement, null, style, this._propertyMap); propertyElement = propertyElement.nextSibling; } if (!this._styleMap) { this._styleMap = {}; } this._styleMap[styleId] = style; } styleElement = styleElement.nextSibling; } }, /** * Process an "ss" directive to load and install a new stylesheet onto the application. * * @param {Element} ssElement the directive element */ _processStyleSheet: function(ssElement) { var styleSheet = Echo.Serial.loadStyleSheet(this.client, ssElement); this.client.application.setStyleSheet(styleSheet); }, /** * Process an "up" directive to update properties and/or add children to a component. * * @param {Element} upElement the directive element */ _processUpdate: function(upElement) { // Determine parent component var parentComponent; if (upElement.getAttribute("r") == "true") { parentComponent = this.client.application.rootComponent; } else { var parentId = upElement.getAttribute("i"); parentComponent = this.client.application.getComponentByRenderId(parentId); } // Child insertion cursor index (if index is omitted, children are added at this position). var cursorIndex = 0; var element = upElement.firstChild; while (element) { if (element.nodeType == 1) { switch (element.nodeName) { case "c": // Added child var component = Echo.Serial.loadComponent(this.client, element, this._propertyMap, this._styleMap); var index = element.getAttribute("x"); if (index == null) { // No index specified, add children at current insertion cursor position. parentComponent.add(component, cursorIndex); ++cursorIndex; } else { // Index specified, add child at index, set insertion cursor position to index + 1. index = parseInt(index, 10); parentComponent.add(component, index); cursorIndex = index + 1; } break; case "p": // Property update Echo.Serial.loadProperty(this.client, element, parentComponent, null, this._propertyMap); break; case "s": // Style name update parentComponent.setStyleName(element.firstChild ? element.firstChild.nodeValue : null); break; case "sr": // Style reference update if (element.firstChild) { parentComponent.setStyle(this._styleMap ? this._styleMap[element.firstChild.nodeValue] : null); } else { parentComponent.setStyle(null); } break; case "e": // Event update var eventType = element.getAttribute("t"); if (element.getAttribute("v") == "true") { this.client.removeComponentListener(parentComponent, eventType); this.client.addComponentListener(parentComponent, eventType); } else { this.client.removeComponentListener(parentComponent, eventType); } break; case "en": // Enabled state update parentComponent.setEnabled(element.firstChild.nodeValue == "true"); break; case "locale": // Locale update parentComponent.setLocale(element.firstChild ? element.firstChild.nodeValue : null); break; case "dir": // Layout direction update parentComponent.setLayoutDirection(element.firstChild ? (element.firstChild.nodeValue == "rtl" ? Echo.LayoutDirection.RTL : Echo.LayoutDirection.LTR) : null); break; } } element = element.nextSibling; } } });




© 2015 - 2024 Weber Informatics LLC | Privacy Policy