From 8bf2eb7614bc060930e35c60aca98ecccbb2c0e4 Mon Sep 17 00:00:00 2001 From: Heng Sin Low Date: Sun, 29 Apr 2012 04:38:26 +0800 Subject: [PATCH] IDEMPIERE-175 Performance: Use atmosphere ( long pooling, NIO ) server push --- .../src/org/adempiere/util/ServerContext.java | 3 +- org.adempiere.ui.zk/.classpath | 1 - org.adempiere.ui.zk/META-INF/MANIFEST.MF | 27 +- .../zk/atmosphere/AtmosphereServerPush.java | 86 +- .../zk/atmosphere/ZkAtmosphereHandler.java | 3 +- .../org/adempiere/webui/AdempiereWebUI.java | 6 + .../webui/dashboard/DPActivities.java | 2 +- .../webui/session/SessionContextListener.java | 28 +- .../webui/util/ServerPushTemplate.java | 4 + .../js/jawwa/atmosphere/jquery.atmosphere.js | 1921 +++++++++++++++++ .../src/web/js/jawwa/atmosphere/serverpush.js | 63 +- .../src/web/js/jawwa/atmosphere/zk.wpd | 1 + org.adempiere.ui.zk/build.properties | 3 +- 13 files changed, 2064 insertions(+), 84 deletions(-) create mode 100644 org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js diff --git a/org.adempiere.base/src/org/adempiere/util/ServerContext.java b/org.adempiere.base/src/org/adempiere/util/ServerContext.java index a410e4ffd7..69bea17b04 100644 --- a/org.adempiere.base/src/org/adempiere/util/ServerContext.java +++ b/org.adempiere.base/src/org/adempiere/util/ServerContext.java @@ -17,6 +17,7 @@ package org.adempiere.util; +import java.io.Serializable; import java.util.Properties; /** @@ -25,7 +26,7 @@ import java.util.Properties; * @date Feb 25, 2007 * @version $Revision: 0.10 $ */ -public final class ServerContext +public final class ServerContext implements Serializable { /** * generated serial version Id diff --git a/org.adempiere.ui.zk/.classpath b/org.adempiere.ui.zk/.classpath index 57cddb3f73..2121688dc1 100644 --- a/org.adempiere.ui.zk/.classpath +++ b/org.adempiere.ui.zk/.classpath @@ -4,7 +4,6 @@ - diff --git a/org.adempiere.ui.zk/META-INF/MANIFEST.MF b/org.adempiere.ui.zk/META-INF/MANIFEST.MF index 265a1e0172..e409bce4fa 100644 --- a/org.adempiere.ui.zk/META-INF/MANIFEST.MF +++ b/org.adempiere.ui.zk/META-INF/MANIFEST.MF @@ -10,13 +10,15 @@ Import-Package: javax.servlet, org.apache.commons.codec.binary, org.apache.ecs, org.apache.ecs.xhtml, - org.osgi.framework;version="1.5.0" + org.osgi.framework;version="1.5.0", + org.slf4j;version="1.6.1", + org.slf4j.helpers;version="1.6.1", + org.slf4j.spi;version="1.6.1" Bundle-ClassPath: WEB-INF/classes/, WEB-INF/lib/atmosphere-runtime-0.9.jar, WEB-INF/lib/atmosphere-compat-jbossweb-0.9.jar, WEB-INF/lib/atmosphere-compat-tomcat-0.9.jar, - WEB-INF/lib/atmosphere-compat-tomcat7-0.9.jar, - WEB-INF/lib/slf4j-api-1.6.1.jar + WEB-INF/lib/atmosphere-compat-tomcat7-0.9.jar Export-Package: metainfo.zk, org.adempiere.webui, org.adempiere.webui.acct, @@ -40,24 +42,7 @@ Export-Package: metainfo.zk, org.adempiere.webui.session, org.adempiere.webui.theme, org.adempiere.webui.util, - org.adempiere.webui.window, - org.atmosphere.cache, - org.atmosphere.client, - org.atmosphere.config, - org.atmosphere.container, - org.atmosphere.container.version, - org.atmosphere.cpr, - org.atmosphere.di, - org.atmosphere.handler, - org.atmosphere.util, - org.atmosphere.util.uri, - org.atmosphere.websocket, - org.atmosphere.websocket.protocol, - org.jboss.servlet.http, - org.apache.catalina, - org.apache.catalina.comet, - org.slf4j.helpers, - org.slf4j.spi + org.adempiere.webui.window Require-Bundle: org.adempiere.report.jasper;bundle-version="1.0.0", org.adempiere.base;bundle-version="1.0.0", org.adempiere.report.jasper.library;bundle-version="1.0.0", diff --git a/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java index 0baba0e28e..c3821343af 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java +++ b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java @@ -19,6 +19,7 @@ the License. */ package fi.jawsy.jawwa.zk.atmosphere; +import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; import org.atmosphere.cpr.AtmosphereResource; @@ -112,17 +113,35 @@ public class AtmosphereServerPush implements ServerPush { return true; } - public void clearResource(AtmosphereResource resource) { + public synchronized void clearResource(AtmosphereResource resource) { this.resource.compareAndSet(resource, null); } - private void commitResponse() { + private synchronized void commitResponse() throws IOException { AtmosphereResource resource = this.resource.getAndSet(null); if (resource != null) { - resource.resume(); + resource.resume(); } } + private synchronized void onPush() throws IOException { + AtmosphereResource resource = this.resource.get(); + if (resource != null) { + switch (resource.transport()) { + case JSONP: + case LONG_POLLING: + if (resource.isSuspended()) + commitResponse(); + break; + case WEBSOCKET : + case STREAMING: + resource.getResponse().getWriter().write("@"); + resource.getResponse().getWriter().flush(); + break; + } + } + } + @Override public boolean deactivate(boolean stop) { boolean stopped = false; @@ -157,10 +176,17 @@ public class AtmosphereServerPush implements ServerPush { } @Override - public void schedule(EventListener task, T event, + public synchronized void schedule(EventListener task, T event, Scheduler scheduler) { + boolean pendingPush = ((DesktopCtrl)desktop.get()).scheduledServerPush(); scheduler.schedule(task, event); - commitResponse(); + if (!pendingPush || (this.resource.get() != null && this.resource.get().isSuspended())) { + try { + onPush(); + } catch (IOException e) { + log.warn(e.getLocalizedMessage(), e); + } + } } @Override @@ -187,32 +213,40 @@ public class AtmosphereServerPush implements ServerPush { log.debug("Stopping server push for " + desktop); Clients.response("jawwa.atmosphere.serverpush", new AuScript(null, "jawwa.atmosphere.stopServerPush('" + desktop.getId() + "');")); - commitResponse(); + try { + commitResponse(); + } catch (IOException e) { + } } - public void updateResource(AtmosphereResource resource) { - commitResponse(); + public synchronized void onRequest(AtmosphereResource resource) { + if (this.resource.get() != null) { + AtmosphereResource aResource = this.resource.get(); + if (aResource.isSuspended()) { + try { + commitResponse(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } - boolean shouldSuspend = true; - Desktop desktop = this.desktop.get(); - if (desktop == null) { - return; - } - - if (desktop instanceof DesktopCtrl) { - DesktopCtrl desktopCtrl = (DesktopCtrl) desktop; - shouldSuspend = !desktopCtrl.scheduledServerPush(); - } - - if (shouldSuspend) { - resource.suspend(timeout, false); - this.resource.set(resource); - } else { - this.resource.set(null); - } + Desktop desktop = this.desktop.get(); + if (desktop != null && desktop instanceof DesktopCtrl) + { + if (((DesktopCtrl)desktop).scheduledServerPush()) + { + return; + } + } + + this.resource.set(resource); + if (!resource.isSuspended()) { + resource.suspend(-1, true); + } } - private static class ThreadInfo { + private static class ThreadInfo { private final Thread thread; /** # of activate() was called. */ private int nActive; diff --git a/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/ZkAtmosphereHandler.java b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/ZkAtmosphereHandler.java index 69fd8f327d..0d5aedbdb6 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/ZkAtmosphereHandler.java +++ b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/ZkAtmosphereHandler.java @@ -118,11 +118,12 @@ public class ZkAtmosphereHandler implements AtmosphereHandler { if (error != null && serverPushEither.getRightValue() == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write(error); + response.getWriter().flush(); return; } AtmosphereServerPush serverPush = serverPushEither.getRightValue(); - serverPush.updateResource(resource); + serverPush.onRequest(resource); } @Override diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/AdempiereWebUI.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/AdempiereWebUI.java index d931a9494e..e8f98e7e6d 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/AdempiereWebUI.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/AdempiereWebUI.java @@ -24,6 +24,7 @@ import java.util.Properties; import javax.servlet.ServletRequest; import javax.servlet.http.HttpSession; +import org.adempiere.util.ServerContext; import org.adempiere.webui.apps.AEnv; import org.adempiere.webui.component.DrillCommand; import org.adempiere.webui.component.TokenCommand; @@ -127,6 +128,8 @@ public class AdempiereWebUI extends Window implements EventListener, IWeb loginCompleted(); } + Executions.getCurrent().getDesktop().enableServerPush(true); + Executions.getCurrent().getDesktop().addListener(new DrillCommand()); Executions.getCurrent().getDesktop().addListener(new TokenCommand()); Executions.getCurrent().getDesktop().addListener(new ZoomCommand()); @@ -283,6 +286,9 @@ public class AdempiereWebUI extends Window implements EventListener, IWeb ctx.put(ZK_DESKTOP_SESSION_KEY, this.getPage().getDesktop()); } + //update session context + currSess.setAttribute(SessionContextListener.SESSION_CTX, ServerContext.getCurrentInstance()); + if ("Y".equalsIgnoreCase(Env.getContext(ctx, BrowserToken.REMEMBER_ME)) && MSystem.isZKRememberUserAllowed()) { MUser user = MUser.get(ctx); diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/dashboard/DPActivities.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/dashboard/DPActivities.java index 5e002f58a4..1e1fa79067 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/dashboard/DPActivities.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/dashboard/DPActivities.java @@ -41,7 +41,7 @@ import org.zkoss.zul.Vbox; * Contributors: * CarlosRuiz - globalqss - Add unprocessed button to iDempiere */ -public class DPActivities extends DashboardPanel implements EventListener { +public class DPActivities extends DashboardPanel implements EventListener { /** * diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/session/SessionContextListener.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/session/SessionContextListener.java index 1bf8869798..ba19b52f57 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/session/SessionContextListener.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/session/SessionContextListener.java @@ -59,7 +59,7 @@ public class SessionContextListener implements ExecutionInit, * @param exec * @param createNew */ - private void setupExecutionContextFromSession(Execution exec) { + public static void setupExecutionContextFromSession(Execution exec) { Session session = exec.getDesktop().getSession(); Properties ctx = (Properties)session.getAttribute(SESSION_CTX); HttpSession httpSession = (HttpSession)session.getNativeSession(); @@ -115,7 +115,7 @@ public class SessionContextListener implements ExecutionInit, * @param errs * @see ExecutionCleanup#cleanup(Execution, Execution, List) */ - public void cleanup(Execution exec, Execution parent, List errs) + public void cleanup(Execution exec, Execution parent, List errs) { //in servlet thread if (parent == null) @@ -140,7 +140,7 @@ public class SessionContextListener implements ExecutionInit, //check is thread local context have been setup if (ServerContext.getCurrentInstance().isEmpty() || !isContextValid()) { - setupExecutionContextFromSession(Executions.getCurrent()); + setupExecutionContextFromSession(Executions.getCurrent()); } } @@ -260,12 +260,12 @@ public class SessionContextListener implements ExecutionInit, * @param errs * @see EventThreadCleanup#cleanup(Component, Event, List) */ - public void cleanup(Component comp, Event evt, List errs) throws Exception + public void cleanup(Component comp, Event evt, List errs) throws Exception { //in event processing thread } - private boolean isContextValid() { + public static boolean isContextValid() { Execution exec = Executions.getCurrent(); Properties ctx = ServerContext.getCurrentInstance(); if (ctx == null) @@ -278,6 +278,24 @@ public class SessionContextListener implements ExecutionInit, { return false; } + + Properties sessionCtx = (Properties) session.getAttribute(SESSION_CTX); + if (sessionCtx != null) + { + if (Env.getAD_Client_ID(sessionCtx) != Env.getAD_Client_ID(ctx)) + { + return false; + } + if (Env.getAD_User_ID(sessionCtx) != Env.getAD_User_ID(ctx)) + { + return false; + } + if (Env.getAD_Role_ID(sessionCtx) != Env.getAD_Role_ID(ctx)) + { + return false; + } + } + return true; } } diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/util/ServerPushTemplate.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/util/ServerPushTemplate.java index 306ce82771..b165db6aef 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/util/ServerPushTemplate.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/util/ServerPushTemplate.java @@ -14,6 +14,7 @@ package org.adempiere.webui.util; import org.adempiere.exceptions.AdempiereException; +import org.adempiere.webui.session.SessionContextListener; import org.zkoss.zk.ui.Desktop; import org.zkoss.zk.ui.DesktopUnavailableException; import org.zkoss.zk.ui.Executions; @@ -48,6 +49,9 @@ public class ServerPushTemplate { EventListener task = new EventListener() { @Override public void onEvent(Event event) throws Exception { + if (!SessionContextListener.isContextValid()) { + SessionContextListener.setupExecutionContextFromSession(desktop.getExecution()); + } callback.updateUI(); } }; diff --git a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js new file mode 100644 index 0000000000..1b90e0df6d --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js @@ -0,0 +1,1921 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Part of this code has been taked from + * + * jQuery Stream @VERSION + * Comet Streaming JavaScript Library + * http://code.google.com/p/jquery-stream/ + * + * Copyright 2011, Donghwan Kim + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Compatible with jQuery 1.5+ + */ +jQuery.atmosphere = function() { + jQuery(window).unload(function() { + jQuery.atmosphere.unsubscribe(); + }); + + var parseHeaders = function(headerString) { + var match, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, headers = {}; + while (match = rheaders.exec(headerString)) { + headers[match[1]] = match[2]; + } + return headers; + }; + + return { + version : 0.9, + requests : [], + callbacks : [], + + onError : function(response) { + }, + onClose : function(response) { + }, + onOpen : function(response) { + }, + onMessage : function(response) { + }, + onReconnect : function(request, response) { + }, + onMessagePublished : function(response) { + }, + + AtmosphereRequest : function(options) { + + /** + * {Object} Request parameters. + * @private + */ + var _request = { + timeout: 300000, + method: 'GET', + headers: {}, + contentType : '', + cache: true, + async: true, + ifModified: false, + callback: null, + dataType: '', + url : '', + data : '', + suspend : true, + maxRequest : 60, + maxStreamingLength : 10000000, + lastIndex : 0, + logLevel : 'info', + requestCount : 0, + fallbackMethod: 'GET', + fallbackTransport : 'streaming', + transport : 'long-polling', + webSocketImpl: null, + webSocketUrl: null, + webSocketPathDelimiter: "@@", + enableXDR : false, + rewriteURL : false, + attachHeadersAsQueryString : true, + executeCallbackBeforeReconnect : false, + readyState : 0, + lastTimestamp : 0, + withCredentials : false, + trackMessageLength : false , + messageDelimiter : '|', + connectTimeout : -1, + onError : function(response) { + }, + onClose : function(response) { + }, + onOpen : function(response) { + }, + onMessage : function(response) { + }, + onReconnect : function(request, response) { + }, + onMessagePublished : function(response) { + } + }; + + /** + * {Object} Request's last response. + * @private + */ + var _response = { + status: 200, + responseBody : '', + expectedBodySize : -1, + headers : [], + state : "messageReceived", + transport : "polling", + error: null, + id : 0 + }; + + /** + * {number} Request id. + * + * @private + */ + var _uuid = 0; + + /** + * {websocket} Opened web socket. + * + * @private + */ + var _websocket = null; + + /** + * {SSE} Opened SSE. + * + * @private + */ + var _sse = null; + + /** + * {XMLHttpRequest, ActiveXObject} Opened ajax request (in case of + * http-streaming or long-polling) + * + * @private + */ + var _activeRequest = null; + + /** + * {Object} Object use for streaming with IE. + * + * @private + */ + var _ieStream = null; + + /** + * {Object} Object use for jsonp transport. + * + * @private + */ + var _jqxhr = null; + + /** + * {boolean} If request has been subscribed or not. + * + * @private + */ + var _subscribed = true; + + /** + * {number} Number of test reconnection. + * + * @private + */ + var _requestCount = 0; + + /** + * {boolean} If request is currently aborded. + * + * @private + */ + var _abordingConnection = false; + + // Automatic call to subscribe + _subscribe(options); + + /** + * Initialize atmosphere request object. + * + * @private + */ + function _init() { + _uuid = 0; + _subscribed = true; + _abordingConnection = false; + _requestCount = 0; + + _websocket = null; + _sse = null; + _activeRequest = null; + _ieStream = null; + } + + /** + * Re-initialize atmosphere object. + * @private + */ + function _reinit() { + _close(); + _init(); + } + + /** + * Subscribe request using request transport.
+ * If request is currently opened, this one will be closed. + * + * @param {Object} + * Request parameters. + * @private + */ + function _subscribe(options) { + _reinit(); + + _request = jQuery.extend(_request, options); + _uuid = jQuery.atmosphere.guid(); + + _execute(); + } + + /** + * Check if web socket is supported (check for custom implementation + * provided by request object or browser implementation). + * + * @returns {boolean} True if web socket is supported, false + * otherwise. + * @private + */ + function _supportWebsocket() { + return _request.webSocketImpl != null || window.WebSocket || window.MozWebSocket; + } + + /** + * Check if server side events (SSE) is supported (check for custom implementation + * provided by request object or browser implementation). + * + * @returns {boolean} True if web socket is supported, false + * otherwise. + * @private + */ + function _supportSSE() { + return window.EventSource; + } + + /** + * Open request using request transport.
+ * If request transport is 'websocket' but websocket can't be + * opened, request will automatically reconnect using fallback + * transport. + * + * @private + */ + function _execute() { + if (_request.transport != 'websocket' && _request.transport != 'sse') { + _open('opening', _request.transport); + _executeRequest(); + + } else if (_request.transport == 'websocket') { + if (!_supportWebsocket()) { + jQuery.atmosphere.log(_request.logLevel, ["Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"]); + _open('opening', _request.fallbackTransport); + _reconnectWithFallbackTransport(); + } else { + _executeWebSocket(false); + } + } else if (_request.transport == 'sse') { + if (!_supportSSE()) { + jQuery.atmosphere.log(_request.logLevel, ["Server Side Events(SSE) is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"]); + _open('opening', _request.fallbackTransport); + _reconnectWithFallbackTransport(); + } else { + _executeSSE(false); + } + } + } + + /** + * @private + */ + function _open(state, transport) { + var prevState = _response.state; + _response.state = state; + _response.status = 200; + var prevTransport = _response.transport; + _response.transport = transport; + _response.responseBody = ""; + _invokeCallback(); + _response.state = prevState; + _response.transport = prevTransport; + } + + /** + * Execute request using jsonp transport. + * + * @param request + * {Object} request Request parameters, if + * undefined _request object will be used. + * @private + */ + function _jsonp(request) { + var rq = _request; + if ((request != null) && (typeof(request) != 'undefined')) { + rq = request; + } + + var url = rq.url; + var data = rq.data; + if (rq.attachHeadersAsQueryString) { + url = _attachHeaders(rq); + if (data != '') { + url += "&X-Atmosphere-Post-Body=" + data; + } + data = ''; + } + + _jqxhr = jQuery.ajax({ + url : url, + type : rq.method, + dataType: "jsonp", + error : function(jqXHR, textStatus, errorThrown) { + if (jqXHR.status < 300) { + _reconnect(_jqxhr, rq); + } else { + _prepareCallback(textStatus, "error", jqXHR.status, rq.transport); + } + }, + jsonp : "jsonpTransport", + success: function(json) { + if (rq.executeCallbackBeforeReconnect) { + _reconnect(_jqxhr, rq); + } + + var msg = json.message; + if (msg != null && typeof msg != 'string') { + try { + msg = jQuery.stringifyJSON(msg); + } catch (err) { + // The message was partial + } + } + + _prepareCallback(msg, "messageReceived", 200, rq.transport); + + if (!rq.executeCallbackBeforeReconnect) { + _reconnect(_jqxhr, rq); + } + }, + data : rq.data, + beforeSend : function(jqXHR) { + _doRequest(jqXHR, rq, false); + } + }); + } + + /** + * Build websocket object. + * + * @param location + * {string} Web socket url. + * @returns {websocket} Web socket object. + * @private + */ + function _getWebSocket(location) { + if (_request.webSocketImpl != null) { + return _request.webSocketImpl; + } else { + if (window.WebSocket) { + return new WebSocket(location); + } else { + return new MozWebSocket(location); + } + } + } + + /** + * Build web socket url from request url. + * + * @return {string} Web socket url (start with "ws" or "wss" for + * secure web socket). + * @private + */ + function _buildWebSocketUrl() { + var url = _request.url; + url = _attachHeaders(); + if (url.indexOf("http") == -1 && url.indexOf("ws") == -1) { + url = jQuery.atmosphere.parseUri(document.location, url); + } + return url.replace('http:', 'ws:').replace('https:', 'wss:'); + } + + /** + * Build SSE url from request url. + * + * @return a url with Atmosphere's headers + * @private + */ + function _buildSSEUrl() { + var url = _request.url; + url = _attachHeaders(); + return url; + } + + /** + * Open SSE.
+ * Automatically use fallback transport if SSE can't be + * opened. + * + * @private + */ + function _executeSSE(sseOpened) { + + _response.transport = "sse"; + + var location = _buildSSEUrl(_request.url); + + jQuery.atmosphere.log(_request.logLevel, ["Invoking executeSSE"]); + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("Using URL: " + location); + } + _sse = new EventSource(location, {withCredentials: _request.withCredentials}); + + if (_request.connectTimeout > 0) { + _request.id = setTimeout(function() { + if (!sseOpened) { + _sse.close(); + } + }, _request.connectTimeout); + } + + _sse.onopen = function(event) { + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("SSE successfully opened"); + } + + _subscribed = true; + _open(sseOpened ? 're-opening' : 'opening', "sse"); + + sseOpened = true; + + if (_request.method == 'POST') { + _response.state = "messageReceived"; + _sse.send(_request.data); + } + }; + + _sse.onmessage = function(message) { + _response.state = 'messageReceived'; + _response.status = 200; + + var message = message.data; + var skipCallbackInvocation = _trackMessageSize(message, _request, _response); + + if (!skipCallbackInvocation) { + _invokeCallback(); + _response.responseBody = ''; + } + }; + + _sse.onerror = function(message) { + + clearTimeout(_request.id) + _response.state = 'closed'; + _response.responseBody = ""; + _response.status = 200; + _invokeCallback(); + + sseOpened = false; + if (_abordingConnection) { + _abordingConnection = false; + jQuery.atmosphere.log(_request.logLevel, ["SSE closed normally"]); + + } else if (!sseOpened) { + jQuery.atmosphere.log(_request.logLevel, ["SSE failed. Downgrading to fallback transport and resending"]); + _open('opening', _request.fallbackTransport); + _reconnectWithFallbackTransport(); + + } else if ((_subscribed) && (_response.transport == 'sse')) { + if (_requestCount++ < _request.maxRequest) { + _request.requestCount = _requestCount; + _response.responseBody = ""; + _executeSSE(true); + } else { + jQuery.atmosphere.log(_request.logLevel, ["SSE reconnect maximum try reached " + _request.requestCount]); + _onError(); + } + } + }; + } + + /** + * Open web socket.
+ * Automatically use fallback transport if web socket can't be + * opened. + * + * @private + */ + function _executeWebSocket(webSocketOpened) { + + _response.transport = "websocket"; + + var location = _buildWebSocketUrl(_request.url); + + jQuery.atmosphere.log(_request.logLevel, ["Invoking executeWebSocket"]); + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("Using URL: " + location); + } + + _websocket = _getWebSocket(location); + + if (_request.connectTimeout > 0) { + _request.id = setTimeout(function() { + if (!webSocketOpened) { + var _message = { + code : 1002, + reason : "", + wasClean : false + }; + _websocket.onclose(_message); + } + }, _request.connectTimeout); + } + + _websocket.onopen = function(message) { + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("Websocket successfully opened"); + } + + _subscribed = true; + _open(webSocketOpened ? 're-opening' : 'opening', "websocket"); + + webSocketOpened = true; + + if (_request.method == 'POST') { + _response.state = "messageReceived"; + _websocket.send(_request.data); + } + }; + + _websocket.onmessage = function(message) { + if (message.data.indexOf("parent.callback") != -1) { + jQuery.atmosphere.log(_request.logLevel, ["parent.callback no longer supported with 0.8 version and up. Please upgrade"]); + } + + _response.state = 'messageReceived'; + _response.status = 200; + + var message = message.data; + var skipCallbackInvocation = _trackMessageSize(message, _request, _response); + + if (!skipCallbackInvocation) { + _invokeCallback(); + _response.responseBody = ''; + } + }; + + _websocket.onerror = function(message) { + clearTimeout(_request.id) + }; + + _websocket.onclose = function(message) { + var reason = message.reason; + if (reason === "") { + switch (message.code) { + case 1000: + reason = "Normal closure; the connection successfully completed whatever purpose for which " + + "it was created."; + break; + case 1001: + reason = "The endpoint is going away, either because of a server failure or because the " + + "browser is navigating away from the page that opened the connection."; + break; + case 1002: + reason = "The endpoint is terminating the connection due to a protocol error."; + break; + case 1003: + reason = "The connection is being terminated because the endpoint received data of a type it " + + "cannot accept (for example, a text-only endpoint received binary data)."; + break; + case 1004: + reason = "The endpoint is terminating the connection because a data frame was received that " + + "is too large."; + break; + case 1005: + reason = "Unknown: no status code was provided even though one was expected."; + break; + case 1006: + reason = "Connection was closed abnormally (that is, with no close frame being sent)."; + break; + } + } + + jQuery.atmosphere.warn("Websocket closed, reason: " + reason); + jQuery.atmosphere.warn("Websocket closed, wasClean: " + message.wasClean); + + _response.state = 'closed'; + _response.responseBody = ""; + _response.status = 200; + _invokeCallback(); + clearTimeout(_request.id) + + if (_abordingConnection) { + _abordingConnection = false; + jQuery.atmosphere.log(_request.logLevel, ["Websocket closed normally"]); + + } else if (!webSocketOpened) { + jQuery.atmosphere.log(_request.logLevel, ["Websocket failed. Downgrading to Comet and resending"]); + _open('opening', _request.fallbackTransport); + _reconnectWithFallbackTransport(); + + } else if ((_subscribed) && (_response.transport == 'websocket')) { + if (_requestCount++ < _request.maxRequest) { + _request.requestCount = _requestCount; + _response.responseBody = ""; + _executeWebSocket(true); + } else { + jQuery.atmosphere.log(_request.logLevel, ["Websocket reconnect maximum try reached " + _request.requestCount]); + jQuery.atmosphere.warn("Websocket error, reason: " + message.reason); + _onError(); + } + } + }; + } + + function _onError() { + _response.state = 'error'; + _response.responseBody = ""; + _response.status = 500; + _invokeCallback(); + } + + /** + * Track received message and make sure callbacks/functions are only invoked when the complete message + * has been received. + * + * @param message + * @param request + * @param response + */ + function _trackMessageSize(message, request, response) { + if (request.trackMessageLength) { + // The message length is the included within the message + var messageStart = message.indexOf(request.messageDelimiter); + + var length = response.expectedBodySize; + if (messageStart != -1) { + length = message.substring(0, messageStart); + message = message.substring(messageStart + 1); + response.expectedBodySize = length; + } + + if (messageStart != -1) { + response.responseBody = message; + } else { + response.responseBody += message; + } + + if (response.responseBody.length != length) { + return true; + } + } else { + response.responseBody = message; + } + return false; + } + + /** + * Reconnect request with fallback transport.
+ * Used in case websocket can't be opened. + * + * @private + */ + function _reconnectWithFallbackTransport() { + _request.transport = _request.fallbackTransport; + _request.method = _request.fallbackMethod; + _response.transport = _request.fallbackTransport; + _executeRequest(); + } + + /** + * Get url from request and attach headers to it. + * + * @param request + * {Object} request Request parameters, if + * undefined _request object will be used. + * + * @returns {Object} Request object, if undefined, + * _request object will be used. + * @private + */ + function _attachHeaders(request) { + var rq = _request; + if ((request != null) && (typeof(request) != 'undefined')) { + rq = request; + } + + var url = rq.url; + + // If not enabled + if (!rq.attachHeadersAsQueryString) return url; + + // If already added + if (url.indexOf("X-Atmosphere-Framework") != -1) { + return url; + } + + url += (url.indexOf('?') != -1) ? '&' : '?'; + url += "X-Atmosphere-tracking-id=" + _uuid; + url += "&X-Atmosphere-Framework=" + jQuery.atmosphere.version; + url += "&X-Atmosphere-Transport=" + rq.transport; + + if (rq.trackMessageLength) { + url += "&X-Atmosphere-TrackMessageSize=" + "true"; + } + + if (rq.lastTimestamp != undefined) { + url += "&X-Cache-Date=" + rq.lastTimestamp; + } else { + url += "&X-Cache-Date=" + 0; + } + + if (rq.contentType != '') { + url += "&Content-Type=" + rq.contentType; + } + + jQuery.each(rq.headers, function(name, value) { + var h = jQuery.isFunction(value) ? value.call(this, ajaxRequest, request, create) : value; + if (h) { + url += "&" + encodeURIComponent(name) + "=" + encodeURIComponent(h); + } + }); + + return url; + } + + /** + * Build ajax request.
+ * Ajax Request is an XMLHttpRequest object, except for IE6 where + * ajax request is an ActiveXObject. + * + * @return {XMLHttpRequest, ActiveXObject} Ajax request. + * @private + */ + function _buildAjaxRequest() { + var ajaxRequest; + if (jQuery.browser.msie) { + var activexmodes = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"]; + for (var i = 0; i < activexmodes.length; i++) { + try { + ajaxRequest = new ActiveXObject(activexmodes[i]); + } catch(e) { + } + } + + } else if (window.XMLHttpRequest) { + ajaxRequest = new XMLHttpRequest(); + } + return ajaxRequest; + } + + /** + * Execute ajax request.
+ * + * @param request + * {Object} request Request parameters, if + * undefined _request object will be used. + * @private + */ + function _executeRequest(request) { + var rq = _request; + if ((request != null) || (typeof(request) != 'undefined')) { + rq = request; + } + + // CORS fake using JSONP + if ((rq.transport == 'jsonp') || ((rq.enableXDR) && (jQuery.atmosphere.checkCORSSupport()))) { + _jsonp(rq); + return; + } + + if ((rq.transport == 'streaming') && (jQuery.browser.msie)) { + rq.enableXDR && window.XDomainRequest ? _ieXDR(rq) : _ieStreaming(rq); + return; + } + + if ((rq.enableXDR) && (window.XDomainRequest)) { + _ieXDR(rq); + return; + } + + if (rq.requestCount++ < rq.maxRequest) { + var ajaxRequest = _buildAjaxRequest(); + _doRequest(ajaxRequest, rq, true); + + if (rq.suspend) { + _activeRequest = ajaxRequest; + } + + if (rq.transport != 'polling') { + _response.transport = rq.transport; + } + + var error = false; + if (!jQuery.browser.msie) { + ajaxRequest.onerror = function() { + error = true; + try { + _response.status = XMLHttpRequest.status; + } catch(e) { + _response.status = 404; + } + + _response.state = "error"; + _invokeCallback(); + ajaxRequest.abort(); + _activeRequest = null; + }; + } + + ajaxRequest.onreadystatechange = function() { + if (_abordingConnection) { + return; + } + + var skipCallbackInvocation = false; + var update = false; + + // Remote server disconnected us, reconnect. + if (rq.transport == 'streaming' + && (rq.readyState > 2 + && ajaxRequest.readyState == 4)) { + + rq.readyState = 0; + rq.lastIndex = 0; + + _reconnect(ajaxRequest, rq, true); + return; + } + + rq.readyState = ajaxRequest.readyState; + + if (ajaxRequest.readyState == 4) { + if (jQuery.browser.msie) { + update = true; + } else if (rq.transport == 'streaming') { + update = true; + } else if (rq.transport == 'long-polling') { + update = true; + clearTimeout(rq.id); + } + + } else if (!jQuery.browser.msie && ajaxRequest.readyState == 3 && ajaxRequest.status == 200 && rq.transport != 'long-polling') { + update = true; + } else { + clearTimeout(rq.id); + } + + if (update) { + + var tempDate = ajaxRequest.getResponseHeader('X-Cache-Date'); + if (tempDate != null || tempDate != undefined) { + _request.lastTimestamp = tempDate.split(" ").pop(); + } + + var responseText = ajaxRequest.responseText; + this.previousLastIndex = rq.lastIndex; + if (rq.transport == 'streaming') { + var text = responseText.substring(rq.lastIndex, responseText.length); + _response.isJunkEnded = true; + + if (rq.lastIndex == 0 && text.indexOf(""; + var endOfJunkLenght = endOfJunk.length; + var junkEnd = text.indexOf(endOfJunk) + endOfJunkLenght; + + if (junkEnd > endOfJunkLenght && junkEnd != text.length) { + _response.responseBody = text.substring(junkEnd); + } else { + skipCallbackInvocation = true; + } + } else { + var message = responseText.substring(rq.lastIndex, responseText.length); + skipCallbackInvocation = _trackMessageSize(message, rq, _response); + } + rq.lastIndex = responseText.length; + + if (jQuery.browser.opera) { + jQuery.atmosphere.iterate(function() { + if (ajaxRequest.responseText.length > rq.lastIndex) { + try { + _response.status = ajaxRequest.status; + _response.headers = parseHeaders(ajaxRequest.getAllResponseHeaders()); + + // HOTFIX for firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=608735 + if (_request.headers) { + jQuery.each(_request.headers, function(name) { + var v = ajaxRequest.getResponseHeader(name); + if (v) { + _response.headers[name] = v; + } + }); + } + } + catch(e) { + _response.status = 404; + } + _response.state = "messageReceived"; + _response.responseBody = ajaxRequest.responseText.substring(rq.lastIndex); + rq.lastIndex = ajaxRequest.responseText.length; + + _invokeCallback(); + if ((rq.transport == 'streaming') && (ajaxRequest.responseText.length > rq.maxStreamingLength)) { + // Close and reopen connection on large data received + ajaxRequest.abort(); + _doRequest(ajaxRequest, rq, true); + } + } + }, 0); + } + + if (skipCallbackInvocation) { + return; + } + } else { + skipCallbackInvocation = _trackMessageSize(responseText, rq, _response); + rq.lastIndex = responseText.length; + } + + try { + _response.status = ajaxRequest.status; + _response.headers = parseHeaders(ajaxRequest.getAllResponseHeaders()); + + // HOTFIX for firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=608735 + if (_request.headers) { + jQuery.each(_request.headers, function(name) { + var v = ajaxRequest.getResponseHeader(name); + if (v) { + _response.headers[name] = v; + } + }); + } + } catch(e) { + _response.status = 404; + } + + if (rq.suspend) { + _response.state = _response.status == 0 ? "closed" : "messageReceived"; + } else { + _response.state = "messagePublished"; + } + + if (!rq.executeCallbackBeforeReconnect) { + _reconnect(ajaxRequest, rq, false); + } + + // For backward compatibility with Atmosphere < 0.8 + if (_response.responseBody.indexOf("parent.callback") != -1) { + jQuery.atmosphere.log(rq.logLevel, ["parent.callback no longer supported with 0.8 version and up. Please upgrade"]); + } + _invokeCallback(); + + if (rq.executeCallbackBeforeReconnect) { + _reconnect(ajaxRequest, rq, false); + } + + if ((rq.transport == 'streaming') && (responseText.length > rq.maxStreamingLength)) { + // Close and reopen connection on large data received + ajaxRequest.abort(); + _doRequest(ajaxRequest, rq, true); + } else { + _open('re-opening', rq.transport); + } + } + }; + ajaxRequest.send(rq.data); + + if (rq.suspend) { + rq.id = setTimeout(function() { + if (_subscribed) { + ajaxRequest.abort(); + _subscribe(rq); + } + }, rq.timeout); + } + _subscribed = true; + + } else { + jQuery.atmosphere.log(rq.logLevel, ["Max re-connection reached."]); + _onError(); + } + } + + /** + * Do ajax request. + * @param ajaxRequest Ajax request. + * @param request Request parameters. + * @param create If ajax request has to be open. + */ + function _doRequest(ajaxRequest, request, create) { + // Prevent Android to cache request + var url = jQuery.atmosphere.prepareURL(request.url); + + if (create) { + ajaxRequest.open(request.method, url, true); + if (request.connectTimeout > -1) { + request.id = setTimeout(function() { + if (request.requestCount == 0) { + ajaxRequest.abort(); + _prepareCallback("Connect timeout", "closed", 200, request.transport); + } + }, request.connectTimeout); + } + } + + if (_request.withCredentials) { + if ("withCredentials" in ajaxRequest) { + ajaxRequest.withCredentials = true; + } + } + + ajaxRequest.setRequestHeader("X-Atmosphere-Framework", jQuery.atmosphere.version); + ajaxRequest.setRequestHeader("X-Atmosphere-Transport", request.transport); + if (request.lastTimestamp != undefined) { + ajaxRequest.setRequestHeader("X-Cache-Date", request.lastTimestamp); + } else { + ajaxRequest.setRequestHeader("X-Cache-Date", 0); + } + + if (request.trackMessageLength) { + ajaxRequest.setRequestHeader("X-Atmosphere-TrackMessageSize", "true") + } + + if (request.contentType != '') { + ajaxRequest.setRequestHeader("Content-Type", request.contentType); + } + ajaxRequest.setRequestHeader("X-Atmosphere-tracking-id", _uuid); + + jQuery.each(request.headers, function(name, value) { + var h = jQuery.isFunction(value) ? value.call(this, ajaxRequest, request, create, _response) : value; + if (h) { + ajaxRequest.setRequestHeader(name, h); + } + }); + } + + function _reconnect(ajaxRequest, request, force) { + if (force || (request.suspend && ajaxRequest.status == 200 && request.transport != 'streaming' && _subscribed)) { + _executeRequest(); + } + } + + // From jquery-stream, which is APL2 licensed as well. + function _ieXDR(request) { + _ieStream = _configureXDR(request); + _ieStream.open(); + } + + // From jquery-stream + function _configureXDR(request) { + var rq = _request; + if ((request != null) && (typeof(request) != 'undefined')) { + rq = request; + } + + var lastMessage = ""; + var transport = rq.transport; + var lastIndex = 0; + + var xdrCallback = function (xdr) { + var responseBody = xdr.responseText; + var isJunkEnded = false; + + if (responseBody.indexOf(""; + var endOfJunkLenght = endOfJunk.length; + var junkEnd = responseBody.indexOf(endOfJunk) + endOfJunkLenght; + + responseBody = responseBody.substring(junkEnd + lastIndex); + lastIndex += responseBody.length; + } + + _prepareCallback(responseBody, "messageReceived", 200, transport); + }; + + var xdr = new window.XDomainRequest(); + var rewriteURL = rq.rewriteURL || function(url) { + // Maintaining session by rewriting URL + // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url + var rewriters = { + JSESSIONID: function(sid) { + return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1"); + }, + PHPSESSID: function(sid) { + return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, ""); + } + }; + + for (var name in rewriters) { + // Finds session id from cookie + var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie); + if (matcher) { + return rewriters[name](matcher[1]); + } + } + + return url; + }; + + // Handles open and message event + xdr.onprogress = function() { + xdrCallback(xdr); + }; + // Handles error event + xdr.onerror = function() { + _prepareCallback(xdr.responseText, "error", 500, transport); + }; + // Handles close event + xdr.onload = function() { + if (lastMessage != xdr.responseText) { + xdrCallback(xdr); + } + if (rq.transport == "long-polling") { + _executeRequest(); + } + }; + + return { + open: function() { + if (rq.method == 'POST') { + rq.attachHeadersAsQueryString = true; + } + var url = _attachHeaders(rq); + if (rq.method == 'POST') { + url += "&X-Atmosphere-Post-Body=" + rq.data; + } + xdr.open(rq.method, rewriteURL(url)); + xdr.send(); + if (rq.connectTimeout > -1) { + rq.id = setTimeout(function() { + if (rq.requestCount == 0) { + xdr.abort(); + _prepareCallback("Connect timeout", "closed", 200, rq.transport); + } + }, rq.connectTimeout); + } + }, + close: function() { + xdr.abort(); + _prepareCallback(xdr.responseText, "closed", 200, transport); + } + }; + } + + // From jquery-stream, which is APL2 licensed as well. + function _ieStreaming(request) { + _ieStream = _configureIE(request); + _ieStream.open(); + } + + function _configureIE(request) { + var rq = _request; + if ((request != null) && (typeof(request) != 'undefined')) { + rq = request; + } + + var stop; + var doc = new window.ActiveXObject("htmlfile"); + + doc.open(); + doc.close(); + + var url = rq.url; + + if (rq.transport != 'polling') { + _response.transport = rq.transport; + } + + return { + open: function() { + var iframe = doc.createElement("iframe"); + + url = _attachHeaders(rq); + if (rq.data != '') { + url += "&X-Atmosphere-Post-Body=" + rq.data; + } + + // Finally attach a timestamp to prevent Android and IE caching. + url = jQuery.atmosphere.prepareURL(url); + + iframe.src = url; + doc.body.appendChild(iframe); + + // For the server to respond in a consistent format regardless of user agent, we polls response text + var cdoc = iframe.contentDocument || iframe.contentWindow.document; + + stop = jQuery.atmosphere.iterate(function() { + if (!cdoc.firstChild) { + return; + } + + // Detects connection failure + if (cdoc.readyState === "complete") { + try { + jQuery.noop(cdoc.fileSize); + } catch(e) { + _prepareCallback("Connection Failure", "error", 500, rq.transport); + return false; + } + } + + var res = cdoc.body ? cdoc.body.lastChild : cdoc; + var readResponse = function() { + // Clones the element not to disturb the original one + var clone = res.cloneNode(true); + + // If the last character is a carriage return or a line feed, IE ignores it in the innerText property + // therefore, we add another non-newline character to preserve it + clone.appendChild(cdoc.createTextNode(".")); + + var text = clone.innerText; + var isJunkEnded = true; + + if (text.indexOf(""; + var endOfJunkLenght = endOfJunk.length; + var junkEnd = text.indexOf(endOfJunk) + endOfJunkLenght; + + text = text.substring(junkEnd); + } + return text.substring(0, text.length - 1); + }; + + //To support text/html content type + if (!jQuery.nodeName(res, "pre")) { + // Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped + // it is deprecated in HTML5, but still works + var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement || cdoc; + var script = cdoc.createElement("script"); + + script.text = "document.write('')"; + + head.insertBefore(script, head.firstChild); + head.removeChild(script); + + // The plaintext element will be the response container + res = cdoc.body.lastChild; + } + + // Handles open event + _prepareCallback(readResponse(), "opening", 200, rq.transport); + + // Handles message and close event + stop = jQuery.atmosphere.iterate(function() { + var text = readResponse(); + if (text.length > rq.lastIndex) { + _response.status = 200; + _prepareCallback(text, "messageReceived", 200, rq.transport); + + // Empties response every time that it is handled + res.innerText = ""; + rq.lastIndex = 0; + } + + if (cdoc.readyState === "complete") { + _prepareCallback("", "re-opening", 200, rq.transport); + _ieStreaming(rq); + return false; + } + }, null); + + return false; + }); + }, + + close: function() { + if (stop) { + stop(); + } + + doc.execCommand("Stop"); + _prepareCallback("", "closed", 200, rq.transport); + } + }; + } + + /** + * Send message. <br> + * Will be automatically dispatch to other connected. + * + * @param {Object, + * string} Message to send. + * @private + */ + function _push(message) { + if (_activeRequest != null || _sse != null) { + _pushAjaxMessage(message); + } else if (_ieStream != null) { + _pushIE(message); + } else if (_jqxhr != null) { + _pushJsonp(message); + } else if (_websocket != null) { + _pushWebSocket(message); + } + } + + /** + * Send a message using currently opened ajax request (using + * http-streaming or long-polling). <br> + * + * @param {string, Object} Message to send. This is an object, string + * message is saved in data member. + * @private + */ + function _pushAjaxMessage(message) { + var rq = _getPushRequest(message); + _executeRequest(rq); + } + + /** + * Send a message using currently opened ie streaming (using + * http-streaming or long-polling). <br> + * + * @param {string, Object} Message to send. This is an object, string + * message is saved in data member. + * @private + */ + function _pushIE(message) { + _pushAjaxMessage(message); + } + + /** + * Send a message using jsonp transport. <br> + * + * @param {string, Object} Message to send. This is an object, string + * message is saved in data member. + * @private + */ + function _pushJsonp(message) { + _pushAjaxMessage(message); + } + + function _getStringMessage(message) { + var msg = message; + if (typeof(msg) == 'object') { + msg = message.data; + } + return msg; + } + + /** + * Build request use to push message using method 'POST' <br>. + * Transport is defined as 'polling' and 'suspend' is set to false. + * + * @return {Object} Request object use to push message. + * @private + */ + function _getPushRequest(message) { + var msg = _getStringMessage(message); + + var rq = { + connected: false, + timeout: 60000, + method: 'POST', + url: _request.url, + contentType : _request.contentType, + headers: {}, + cache: true, + async: true, + ifModified: false, + callback: null, + dataType: '', + data : msg, + suspend : false, + maxRequest : 60, + logLevel : 'info', + requestCount : 0, + transport: 'polling' + }; + + if (typeof(message) == 'object') { + rq = $.extend(rq, message); + } + + return rq; + } + + /** + * Send a message using currently opened websocket. <br> + * + * @param {string, Object} + * Message to send. This is an object, string message is + * saved in data member. + */ + function _pushWebSocket(message) { + var msg = _getStringMessage(message); + var data; + try { + if (_request.webSocketUrl != null) { + data = _request.webSocketPathDelimiter + + _request.webSocketUrl + + _request.webSocketPathDelimiter + + msg; + } else { + data = msg; + } + + _websocket.send(data); + + } catch (e) { + jQuery.atmosphere.log(_request.logLevel, ["Websocket failed. Downgrading to Comet and resending " + data]); + + _websocket.onclose = function(message) { + }; + _websocket.close(); + + _reconnectWithFallbackTransport(); + _pushAjaxMessage(message); + } + } + + function _prepareCallback(messageBody, state, errorCode, transport) { + + if (state == "messageReceived") { + if (_trackMessageSize(messageBody, _request, _response)) return; + } + + _response.transport = transport; + _response.status = errorCode; + + // If not -1, we have buffered the message. + if (_response.expectedBodySize == -1) { + _response.responseBody = messageBody; + } + _response.state = state; + + _invokeCallback(); + } + + function _invokeFunction(response) { + _f(response, _request); + // Global + _f(response, jQuery.atmosphere); + } + + function _f(response, f) { + switch (response.state) { + case "messageReceived" : + if (typeof(f.onMessage) != 'undefined') f.onMessage(response); + break; + case "error" : + if (typeof(f.onError) != 'undefined') f.onError(response); + break; + case "opening" : + if (typeof(f.onOpen) != 'undefined') f.onOpen(response); + break; + case "messagePublished" : + if (typeof(f.onMessagePublished) != 'undefined') f.onMessagePublished(response); + break; + case "re-opening" : + if (typeof(f.onReconnect) != 'undefined') f.onReconnect(_request, response); + break; + case "closed" : + if (typeof(f.onClose) != 'undefined') f.onClose(response); + break; + } + } + + /** + * Invoke request callbacks. + * + * @private + */ + function _invokeCallback() { + var call = function (index, func) { + func(_response); + }; + + _invokeFunction(_response); + + // Invoke global callbacks + if (jQuery.atmosphere.callbacks.length > 0) { + jQuery.atmosphere.debug("Invoking " + jQuery.atmosphere.callbacks.length + " global callbacks: " + _response.state); + try { + jQuery.each(jQuery.atmosphere.callbacks, call); + } catch (e) { + jQuery.atmosphere.log(_request.logLevel, ["Callback exception" + e]); + } + } + + // Invoke request callback + if (typeof(_request.callback) == 'function') { + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("Invoking request callbacks"); + } + try { + _request.callback(_response); + } catch (e) { + jQuery.atmosphere.log(_request.logLevel, ["Callback exception" + e]); + } + } + } + + /** + * Close request. + * + * @private + */ + function _close() { + _subscribed = false; + _abordingConnection = true; + _response.state = 'unsubscribe'; + _response.responseBody = ""; + _response.status = 408; + _invokeCallback(); + + if (_ieStream != null) { + _ieStream.close(); + _ieStream = null; + _abordingConnection = false; + } + if (_jqxhr != null) { + _jqxhr.abort(); + _jqxhr = null; + _abordingConnection = false; + } + if (_activeRequest != null) { + _activeRequest.abort(); + _activeRequest = null; + _abordingConnection = false; + } + if (_websocket != null) { + _closingWebSocket = true; + _websocket.close(); + _websocket = null; + } + if (_sse != null) { + _closingSSE = true; + _sse.close(); + _sse = null; + } + } + + this.subscribe = function(options) { + _subscribe(options); + }; + + this.execute = function() { + _execute(); + }; + + this.invokeCallback = function() { + _invokeCallback(); + }; + + this.close = function() { + _close(); + }; + + this.getUrl = function() { + return _request.url; + }; + + this.push = function(message) { + _push(message); + } + + this.response = _response; + }, + + subscribe: function(url, callback, request) { + if (typeof(callback) == 'function') { + jQuery.atmosphere.addCallback(callback); + } + + if (typeof(url) != "string") { + request = url; + } else { + request.url = url; + } + + var rq = new jQuery.atmosphere.AtmosphereRequest(request); + jQuery.atmosphere.requests[jQuery.atmosphere.requests.length] = rq; + return rq; + }, + + addCallback: function(func) { + if (jQuery.inArray(func, jQuery.atmosphere.callbacks) == -1) { + jQuery.atmosphere.callbacks.push(func); + } + }, + + removeCallback: function(func) { + var index = jQuery.inArray(func, jQuery.atmosphere.callbacks); + if (index != -1) { + jQuery.atmosphere.callbacks.splice(index, 1); + } + }, + + unsubscribe : function() { + if (jQuery.atmosphere.requests.length > 0) { + for (var i = 0; i < jQuery.atmosphere.requests.length; i++) { + jQuery.atmosphere.requests[i].close(); + clearTimeout(jQuery.atmosphere.requests[i].id); + } + } + jQuery.atmosphere.requests = []; + jQuery.atmosphere.callbacks = []; + }, + + unsubscribeUrl: function(url) { + var idx = -1; + if (jQuery.atmosphere.requests.length > 0) { + for (var i = 0; i < jQuery.atmosphere.requests.length; i++) { + var rq = jQuery.atmosphere.requests[i]; + + // Suppose you can subscribe once to an url + if (rq.getUrl() == url) { + rq.close(); + clearTimeout(rq.id); + idx = i; + break; + } + } + } + if (idx >= 0) { + jQuery.atmosphere.requests.splice(idx, 1); + } + }, + + publish: function(request) { + if (typeof(request.callback) == 'function') { + jQuery.atmosphere.addCallback(callback); + } + request.transport = "polling"; + + var rq = new jQuery.atmosphere.AtmosphereRequest(request); + jQuery.atmosphere.requests[jQuery.atmosphere.requests.length] = rq; + return rq; + }, + + checkCORSSupport : function() { + if (jQuery.browser.msie && !window.XDomainRequest) { + return true; + } else if (jQuery.browser.opera) { + return true; + } + + // Force Android to use CORS as some version like 2.2.3 fail otherwise + var ua = navigator.userAgent.toLowerCase(); + var isAndroid = ua.indexOf("android") > -1; + if (isAndroid) { + return true; + } + return false; + }, + + S4 : function() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + }, + + guid : function() { + return (jQuery.atmosphere.S4() + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + jQuery.atmosphere.S4() + jQuery.atmosphere.S4()); + }, + + // From jQuery-Stream + prepareURL: function(url) { + // Attaches a time stamp to prevent caching + var ts = jQuery.now(); + var ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts); + + return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : ""); + }, + + // From jQuery-Stream + param : function(data) { + return jQuery.param(data, jQuery.ajaxSettings.traditional); + }, + + iterate : function (fn, interval) { + var timeoutId; + + // Though the interval is 0 for real-time application, there is a delay between setTimeout calls + // For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting + interval = interval || 0; + + (function loop() { + timeoutId = setTimeout(function() { + if (fn() === false) { + return; + } + + loop(); + }, interval); + })(); + + return function() { + clearTimeout(timeoutId); + }; + }, + + parseUri : function(baseUrl, uri) { + var protocol = window.location.protocol; + var host = window.location.host; + var path = window.location.pathname; + var parameters = {}; + var anchor = ''; + var pos; + + if ((pos = uri.search(/\:/)) >= 0) { + protocol = uri.substring(0, pos + 1); + uri = uri.substring(pos + 1); + } + + if ((pos = uri.search(/\#/)) >= 0) { + anchor = uri.substring(pos + 1); + uri = uri.substring(0, pos); + } + + if ((pos = uri.search(/\?/)) >= 0) { + var paramsStr = uri.substring(pos + 1) + '&;'; + uri = uri.substring(0, pos); + while ((pos = paramsStr.search(/\&/)) >= 0) { + var paramStr = paramsStr.substring(0, pos); + paramsStr = paramsStr.substring(pos + 1); + + if (paramStr.length) { + var equPos = paramStr.search(/\=/); + if (equPos < 0) { + parameters[paramStr] = ''; + } else { + parameters[paramStr.substring(0, equPos)] = + decodeURIComponent(paramStr.substring(equPos + 1)); + } + } + } + } + + if (uri.search(/\/\//) == 0) { + uri = uri.substring(2); + if ((pos = uri.search(/\//)) >= 0) { + host = uri.substring(0, pos); + path = uri.substring(pos); + } else { + host = uri; + path = '/'; + } + } else if (uri.search(/\//) == 0) { + path = uri; + } + + else // relative to directory + { + var p = path.lastIndexOf('/'); + if (p < 0) { + path = '/'; + } else if (p < path.length - 1) { + path = path.substring(0, p + 1); + } + + while (uri.search(/\.\.\//) == 0) { + p = path.lastIndexOf('/', path.lastIndexOf('/') - 1); + if (p >= 0) { + path = path.substring(0, p + 1); + } + uri = uri.substring(3); + } + path = path + uri; + } + + var formattedUri = protocol + '//' + host + path; + var div = '?'; + for (var key in parameters) { + formattedUri += div + key + '=' + encodeURIComponent(parameters[key]); + div = '&'; + } + return formattedUri; + }, + + log: function (level, args) { + if (window.console) { + var logger = window.console[level]; + if (typeof logger == 'function') { + logger.apply(window.console, args); + } + } + }, + + warn: function() { + jQuery.atmosphere.log('warn', arguments); + }, + + info :function() { + jQuery.atmosphere.log('info', arguments); + }, + + debug: function() { + jQuery.atmosphere.log('debug', arguments); + }, + + error: function() { + jQuery.atmosphere.log('error', arguments); + } + }; +}(); + +/* + * jQuery stringifyJSON + * http://github.com/flowersinthesand/jquery-stringifyJSON + * + * Copyright 2011, Donghwan Kim + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + */ +// This plugin is heavily based on Douglas Crockford's reference implementation +(function($) { + + var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, meta = { + '\b' : '\\b', + '\t' : '\\t', + '\n' : '\\n', + '\f' : '\\f', + '\r' : '\\r', + '"' : '\\"', + '\\' : '\\\\' + }; + + function quote(string) { + return '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === "string" ? c : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"'; + } + + function f(n) { + return n < 10 ? "0" + n : n; + } + + function str(key, holder) { + var i, v, len, partial, value = holder[key], type = typeof value; + + if (value && typeof value === "object" && typeof value.toJSON === "function") { + value = value.toJSON(key); + type = typeof value; + } + + switch (type) { + case "string": + return quote(value); + case "number": + return isFinite(value) ? String(value) : "null"; + case "boolean": + return String(value); + case "object": + if (!value) { + return "null"; + } + + switch (Object.prototype.toString.call(value)) { + case "[object Date]": + return isFinite(value.valueOf()) ? '"' + value.getUTCFullYear() + "-" + f(value.getUTCMonth() + 1) + "-" + f(value.getUTCDate()) + "T" + + f(value.getUTCHours()) + ":" + f(value.getUTCMinutes()) + ":" + f(value.getUTCSeconds()) + "Z" + '"' : "null"; + case "[object Array]": + len = value.length; + partial = []; + for (i = 0; i < len; i++) { + partial.push(str(i, value) || "null"); + } + + return "[" + partial.join(",") + "]"; + default: + partial = []; + for (i in value) { + if (Object.prototype.hasOwnProperty.call(value, i)) { + v = str(i, value); + if (v) { + partial.push(quote(i) + ":" + v); + } + } + } + + return "{" + partial.join(",") + "}"; + } + } + } + + $.stringifyJSON = function(value) { + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(value); + } + + return str("", {"": value}); + }; + +}(jQuery)); \ No newline at end of file diff --git a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/serverpush.js b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/serverpush.js index fb21c3b571..f04892d009 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/serverpush.js +++ b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/serverpush.js @@ -18,6 +18,7 @@ timeout: 300000, delay: 1000, failures: 0, + count: 0, $init: function(desktop, timeout) { this.desktop = desktop; @@ -34,33 +35,43 @@ _send: function() { if (!this.active) return; - + var me = this; - var jqxhr = $.ajax({ - url: zk.ajaxURI("/comet", { - au: true - }), - type: "GET", - cache: false, - async: true, - global: false, - data: { - dtid: this.desktop.id - }, - dataType: "", - timeout: me.timeout, - transport : 'long-polling', - error: function(jqxhr, textStatus, errorThrown) { - me.failures += 1; - me._schedule(); - }, - success: function(data) { - zAu.cmd0.echo(me.desktop); - me.failures = 0; - me._schedule(); - } - }); - this._req = jqxhr; + + var socket = $.atmosphere; + var request = { + url: zk.ajaxURI("/comet", { + au: true + }), + logLevel : 'debug', + transport : 'streaming', + fallbackTransport: 'long-polling', + method: "GET", + cache: false, + async: true, + timeout: me.timeout, + onError: function(response) { + me.failures += 1; + me.count--; + if (response.transport == 'long-polling' && me.count == 0) { + me._schedule(); + } else if (me.failures >= 10) { + me.stop(); + } + }, + onMessage: function(response) { + zAu.cmd0.echo(me.desktop); + me.failures = 0; + me.count--; + if (response.transport == 'long-polling' && me.count == 0) { + me._schedule(); + } + } + }; + + request.url = request.url+'?dtid='+me.desktop.id; + this.count++; + socket.subscribe(request); }, start: function() { this.desktop._serverpush = this; diff --git a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/zk.wpd b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/zk.wpd index e1579d8edc..875f60749a 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/zk.wpd +++ b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/zk.wpd @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <package name="jawwa.atmosphere" language="xul/html"> + <script src="jquery.atmosphere.js" /> <script src="serverpush.js" /> </package> diff --git a/org.adempiere.ui.zk/build.properties b/org.adempiere.ui.zk/build.properties index 927cbe06e4..4d4a3b21ac 100644 --- a/org.adempiere.ui.zk/build.properties +++ b/org.adempiere.ui.zk/build.properties @@ -14,8 +14,7 @@ bin.includes = META-INF/,\ WEB-INF/lib/atmosphere-runtime-0.9.jar,\ WEB-INF/lib/atmosphere-compat-jbossweb-0.9.jar,\ WEB-INF/lib/atmosphere-compat-tomcat-0.9.jar,\ - WEB-INF/lib/atmosphere-compat-tomcat7-0.9.jar,\ - WEB-INF/lib/slf4j-api-1.6.1.jar + WEB-INF/lib/atmosphere-compat-tomcat7-0.9.jar src.includes = WEB-INF/classes/,\ WEB-INF/tld/,\ WEB-INF/web.xml,\