diff --git a/org.adempiere.ui.zk/.classpath b/org.adempiere.ui.zk/.classpath index e5e200230c..97012df4ab 100644 --- a/org.adempiere.ui.zk/.classpath +++ b/org.adempiere.ui.zk/.classpath @@ -1,8 +1,13 @@ - - - - - - - - + + + + + + + + + + + + + diff --git a/org.adempiere.ui.zk/META-INF/MANIFEST.MF b/org.adempiere.ui.zk/META-INF/MANIFEST.MF index 2b05b7bf43..265a1e0172 100644 --- a/org.adempiere.ui.zk/META-INF/MANIFEST.MF +++ b/org.adempiere.ui.zk/META-INF/MANIFEST.MF @@ -3,7 +3,20 @@ Bundle-ManifestVersion: 2 Bundle-Name: Zk Web Client Bundle-SymbolicName: org.adempiere.ui.zk;singleton:=true Bundle-Version: 1.0.0.qualifier -Web-ContextPath: webui +Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Import-Package: javax.servlet, + javax.servlet.http, + metainfo.zk, + org.apache.commons.codec.binary, + org.apache.ecs, + org.apache.ecs.xhtml, + org.osgi.framework;version="1.5.0" +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 Export-Package: metainfo.zk, org.adempiere.webui, org.adempiere.webui.acct, @@ -27,22 +40,31 @@ Export-Package: metainfo.zk, org.adempiere.webui.session, org.adempiere.webui.theme, org.adempiere.webui.util, - org.adempiere.webui.window + 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 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", org.adempiere.ui;bundle-version="1.0.0", org.zkoss.zk.library;bundle-version="6.0.0" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Eclipse-ExtensibleAPI: true -Import-Package: javax.servlet, - javax.servlet.http, - metainfo.zk, - org.apache.commons.codec.binary, - org.apache.ecs, - org.apache.ecs.xhtml, - org.osgi.framework;version="1.5.0" -Bundle-ActivationPolicy: lazy Bundle-Activator: org.adempiere.webui.WebUIActivator -Bundle-ClassPath: WEB-INF/classes/ +Bundle-ActivationPolicy: lazy +Eclipse-ExtensibleAPI: true Eclipse-RegisterBuddy: org.zkoss.zk.library +Web-ContextPath: webui 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 new file mode 100644 index 0000000000..b6cdf89995 --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java @@ -0,0 +1,366 @@ +package fi.jawsy.jawwa.zk.atmosphere; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.atmosphere.cpr.AtmosphereResource; +import org.zkoss.lang.Library; +import org.zkoss.util.logging.Log; +import org.zkoss.zk.au.out.AuScript; +import org.zkoss.zk.ui.Desktop; +import org.zkoss.zk.ui.DesktopUnavailableException; +import org.zkoss.zk.ui.Execution; +import org.zkoss.zk.ui.Executions; +import org.zkoss.zk.ui.UiException; +import org.zkoss.zk.ui.event.Event; +import org.zkoss.zk.ui.event.EventListener; +import org.zkoss.zk.ui.impl.ExecutionCarryOver; +import org.zkoss.zk.ui.sys.DesktopCtrl; +import org.zkoss.zk.ui.sys.Scheduler; +import org.zkoss.zk.ui.sys.ServerPush; +import org.zkoss.zk.ui.util.Clients; +import org.zkoss.zk.ui.util.Configuration; + +/** + * ZK server push implementation based on Atmosphere. + * + * Only supports asynchronous updates (Executions.schedule) and will throw exceptions if synchronous updates + * (Executions.activate/deactivate) is attempted. + */ +public class AtmosphereServerPush implements ServerPush { + private static final Log log = Log.lookup(AtmosphereServerPush.class); + /** Denote a server-push thread gives up the activation (timeout). */ + private static final int GIVEUP = -99; + + private Desktop _desktop; + /** List of ThreadInfo. */ + private final List _pending = new LinkedList(); + /** The active thread. */ + private ThreadInfo _active; + /** The info to carray over from onPiggyback to the server-push thread. */ + private ExecutionCarryOver _carryOver; +// private final int _min, _max, _factor; + /** A mutex that is used by this object to wait for the server-push thread + * to complete. + */ + private final Object _mutex = new Object(); + + public static final int DEFAULT_TIMEOUT = 1000 * 60 * 5; + private final AtomicReference resource = new AtomicReference(); + private final int timeout; + + public AtmosphereServerPush() { +// this(-1, -1, -1); +// } +// +// public AtmosphereServerPush(int min, int max, int factor) { +// _min = min; +// _max = max; +// _factor = factor; + + String timeoutString = Library.getProperty("fi.jawsy.jawwa.zk.atmosphere.timeout"); + if (timeoutString == null || timeoutString.length() == 0) { + timeout = DEFAULT_TIMEOUT; + } else { + timeout = Integer.valueOf(timeoutString); + } + } + + protected void startClientPush() { +// Clients.response("zk.clientpush", new AuScript(null, getStartScript())); + Clients.response("jawwa.atmosphere.serverpush", new AuScript(null, getStartScript())); + } + + protected void stopClientPush() { +// Clients.response("zk.clientpush", new AuScript(null, getStopScript())); + Clients.response("jawwa.atmosphere.serverpush", new AuScript(null, getStopScript())); + } + + protected String getStartScript() { +// final String start = _desktop.getWebApp().getConfiguration() +// .getPreference("PollingServerPush.start", null); +// if (start != null) +// return start; +// +// final StringBuffer sb = new StringBuffer(128) +// .append("zk.load('zk.cpsp');zk.afterLoad(function(){zk.cpsp.start('") +// .append(_desktop.getId()).append('\''); +// +// final int min = _min > 0 ? _min: getIntPref("PollingServerPush.delay.min"), +// max = _max > 0 ? _max: getIntPref("PollingServerPush.delay.max"), +// factor = _factor > 0 ? _factor: getIntPref("PollingServerPush.delay.factor"); +// if (min > 0 || max > 0 || factor > 0) +// sb.append(',').append(min).append(',').append(max) +// .append(',').append(factor); +// +// return sb.append(");});").toString(); + + int clientTimeout = timeout + 1000 * 60; + return "jawwa.atmosphere.startServerPush('" + _desktop.getId() + "', " + clientTimeout + ");"; + } + +// private int getIntPref(String key) { +// final String s = _desktop.getWebApp().getConfiguration() +// .getPreference(key, null); +// if (s != null) { +// try { +// return Integer.parseInt(s); +// } catch (NumberFormatException ex) { +// log.warning("Not a number specified at "+key); +// } +// } +// return -1; +// } + + protected String getStopScript() { +// final String stop = _desktop.getWebApp().getConfiguration() +// .getPreference("PollingServerPush.stop", null); +// return stop != null ? stop: +// "zk.cpsp.stop('" + _desktop.getId() + "');"; + return "jawwa.atmosphere.stopServerPush('" + _desktop.getId() + "');"; + } + + @Override + public boolean isActive() { + return _active != null && _active.nActive > 0; + } + + @Override + public void start(Desktop desktop) { + if (_desktop != null) { + log.warning("Ignored: Sever-push already started"); + return; + } + + _desktop = desktop; + startClientPush(); + } + + @Override + public void stop() { + if (_desktop == null) { + log.warning("Ignored: Sever-push not started"); + return; + } + + final Execution exec = Executions.getCurrent(); + final boolean inexec = exec != null && exec.getDesktop() == _desktop; + //it might be caused by DesktopCache expunge (when creating another desktop) + try { + if (inexec && _desktop.isAlive()) //Bug 1815480: don't send if timeout + stopClientPush(); + commitResponse(); + } finally { + _desktop = null; //to cause DesktopUnavailableException being thrown + wakePending(); + + //if inexec, either in working thread, or other event listener + //if in working thread, we cannot notify here (too early to wake). + //if other listener, no need notify (since onPiggyback not running) + if (!inexec) { + synchronized (_mutex) { + _mutex.notify(); //wake up onPiggyback + } + } + } + } + + private void wakePending() { + synchronized (_pending) { + for (ThreadInfo info: _pending) { + synchronized (info) { + info.notify(); + } + } + _pending.clear(); + } + } + + @Override + public void onPiggyback() { + final Configuration config = _desktop.getWebApp().getConfiguration(); + long tmexpired = 0; + for (int cnt = 0; !_pending.isEmpty();) { + //Don't hold the client too long. + //In addition, an ill-written code might activate again + //before onPiggyback returns. It causes dead-loop in this case. + if (tmexpired == 0) { //first time + tmexpired = System.currentTimeMillis() + + (config.getMaxProcessTime() >> 1); + cnt = _pending.size() + 3; + } else if (--cnt < 0 || System.currentTimeMillis() > tmexpired) { + break; + } + + final ThreadInfo info; + synchronized (_pending) { + if (_pending.isEmpty()) + return; //nothing to do + info = _pending.remove(0); + } + + //Note: we have to sync _mutex before info. Otherwise, + //sync(info) might cause deactivate() to run before _mutex.wait + synchronized (_mutex) { + _carryOver = new ExecutionCarryOver(_desktop); + + synchronized (info) { + if (info.nActive == GIVEUP) + continue; //give up and try next + info.nActive = 1; //granted + info.notify(); + } + + if (_desktop == null) //just in case + break; + + try { + _mutex.wait(); //wait until the server push is done + } catch (InterruptedException ex) { + throw UiException.Aide.wrap(ex); + } + } + } + } + + @Override + public + void schedule(EventListener listener, T event, Scheduler scheduler) { + scheduler.schedule(listener, event); //delegate back + commitResponse(); + } + + @Override + public boolean activate(long timeout) throws InterruptedException, DesktopUnavailableException { + final Thread curr = Thread.currentThread(); + if (_active != null && _active.thread.equals(curr)) { //re-activate + ++_active.nActive; + return true; + } + + final ThreadInfo info = new ThreadInfo(curr); + synchronized (_pending) { + if (_desktop != null) + _pending.add(info); + } + + boolean loop; + do { + loop = false; + synchronized (info) { + if (_desktop != null) { + if (info.nActive == 0) //not granted yet + info.wait(timeout <= 0 ? 10*60*1000: timeout); + + if (info.nActive <= 0) { //not granted + boolean bTimeout = timeout > 0; + boolean bDead = _desktop == null || !_desktop.isAlive(); + if (bTimeout || bDead) { //not timeout + info.nActive = GIVEUP; //denote timeout (and give up) + synchronized (_pending) { //undo pending + _pending.remove(info); + } + + if (bDead) + throw new DesktopUnavailableException("Stopped"); + return false; //timeout + } + + log.debug("Executions.activate() took more than 10 minutes"); + loop = true; //try again + } + } + } + } while (loop); + + if (_desktop == null) + throw new DesktopUnavailableException("Stopped"); + + _carryOver.carryOver(); + _active = info; + return true; + + //Note: we don't mimic inEventListener since 1) ZK doesn't assume it + //2) Window depends on it + } + + @Override + public boolean deactivate(boolean stop) { + boolean stopped = false; + if (_active != null && + Thread.currentThread().equals(_active.thread)) { + if (--_active.nActive <= 0) { + if (stop) + stopClientPush(); + + _carryOver.cleanup(); + _carryOver = null; + _active.nActive = 0; //just in case + _active = null; + + if (stop) { + wakePending(); + _desktop = null; + stopped = true; + } + + //wake up onPiggyback + synchronized (_mutex) { + _mutex.notify(); + } + + try {Thread.sleep(100);} catch (Throwable ex) {} + //to minimize the chance that the server-push thread + //activate again, before onPiggback polls next _pending + } + } + return stopped; + } + + public void clearResource(AtmosphereResource resource) { + this.resource.compareAndSet(resource, null); + } + + private void commitResponse() { + AtmosphereResource resource = this.resource.getAndSet(null); + if (resource != null) { + resource.resume(); + } + } + + public void updateResource(AtmosphereResource resource) { + commitResponse(); + + boolean shouldSuspend = true; + 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); + } + } + + private static class ThreadInfo { + private final Thread thread; + /** # of activate() was called. */ + private int nActive; + private ThreadInfo(Thread thread) { + this.thread = thread; + } + public String toString() { + return "[" + thread + ',' + 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 new file mode 100644 index 0000000000..d93d3b6a1b --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/ZkAtmosphereHandler.java @@ -0,0 +1,120 @@ +package fi.jawsy.jawwa.zk.atmosphere; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.atmosphere.cpr.AtmosphereHandler; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResource; +import org.atmosphere.cpr.AtmosphereResourceEvent; +import org.atmosphere.cpr.AtmosphereResponse; +import org.zkoss.zk.ui.Desktop; +import org.zkoss.zk.ui.Session; +import org.zkoss.zk.ui.WebApp; +import org.zkoss.zk.ui.http.WebManager; +import org.zkoss.zk.ui.sys.DesktopCtrl; +import org.zkoss.zk.ui.sys.ServerPush; +import org.zkoss.zk.ui.sys.WebAppCtrl; + +/** + * Atmosphere handler that integrates Atmosphere with ZK server push. + */ +public class ZkAtmosphereHandler implements AtmosphereHandler { + private String err; + + private AtmosphereServerPush getServerPush(AtmosphereResource resource) { + AtmosphereRequest request = resource.getRequest(); + + Session session = WebManager.getSession(resource.getAtmosphereConfig().getServletContext(), request, false); + + if (session == null) + { + err = "Could not find session"; + return null; + } + else + { + String desktopId = request.getParameter("dtid"); + if (desktopId == null || desktopId.length() == 0) + { + err = "Could not find desktop id"; + return null; + } + + WebApp webApp = session.getWebApp(); + if (webApp instanceof WebAppCtrl) + { + WebAppCtrl webAppCtrl = (WebAppCtrl) webApp; + Desktop desktop = webAppCtrl.getDesktopCache(session).getDesktopIfAny(desktopId); + if (desktop == null) + { + err = "Could not find desktop"; + return null; + } + + if (desktop instanceof DesktopCtrl) + { + DesktopCtrl desktopCtrl = (DesktopCtrl) desktop; + + ServerPush serverPush = desktopCtrl.getServerPush(); + if (serverPush == null) + { + err = "Server push is not enabled"; + return null; + } + + if (desktopCtrl.getServerPush() instanceof AtmosphereServerPush) + { + AtmosphereServerPush atmosphereServerPush = (AtmosphereServerPush) serverPush; + if (atmosphereServerPush != null) + return atmosphereServerPush; + } + + err = "Server push implementation is not AtmosphereServerPush"; + return null; + } + + err = "Desktop does not implement DesktopCtrl"; + return null; + } + + err = "Webapp does not implement WebAppCtrl"; + return null; + } + } + + @Override + public void onRequest(AtmosphereResource resource) throws IOException { + AtmosphereResponse response = resource.getResponse(); + + response.setContentType("text/plain"); + + AtmosphereServerPush serverPush = getServerPush(resource); + if (serverPush == null) + { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write(err); + return; + } + + serverPush.updateResource(resource); + } + + @Override + public void onStateChange(AtmosphereResourceEvent event) throws IOException { + AtmosphereResource resource = event.getResource(); + + if (event.isCancelled() || event.isResumedOnTimeout()) { + AtmosphereServerPush serverPush = getServerPush(resource); + if (serverPush != null) + serverPush.clearResource(resource); + } + } + + @Override + public void destroy() { + + } + +} diff --git a/org.adempiere.ui.zk/WEB-INF/src/metainfo/zk/lang-addon.xml b/org.adempiere.ui.zk/WEB-INF/src/metainfo/zk/lang-addon.xml index d1c89d0c29..ac27fe4f25 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/metainfo/zk/lang-addon.xml +++ b/org.adempiere.ui.zk/WEB-INF/src/metainfo/zk/lang-addon.xml @@ -38,4 +38,6 @@ Copyright (C) 2007 Ashley G Ramdass (ADempiere WebUI). + + 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 new file mode 100644 index 0000000000..0f24ca8cab --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/serverpush.js @@ -0,0 +1,79 @@ +(function() { + jawwa.atmosphere.startServerPush = function(dtid, timeout) { + var dt = zk.Desktop.$(dtid); + if (dt._serverpush) + dt._serverpush.stop(); + + var spush = new jawwa.atmosphere.ServerPush(dt, timeout); + spush.start(); + }; + jawwa.atmosphere.stopServerPush = function(dtid) { + var dt = zk.Desktop.$(dtid); + if (dt._serverpush) + dt._serverpush.stop(); + }; + jawwa.atmosphere.ServerPush = zk.$extends(zk.Object, { + desktop: null, + active: false, + timeout: 300000, + delay: 1000, + failures: 0, + + $init: function(desktop, timeout) { + this.desktop = desktop; + this.timeout = timeout; + }, + _schedule: function() { + if (this.failures < 10) { + var delay = this.delay * Math.pow(2, Math.min(this.failures, 7)); + setTimeout(this.proxy(this._send), delay); + } else { + this.stop(); + } + }, + _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 + }, + accepts: "text/plain", + dataType: "text/plain", + timeout: me.timeout, + 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; + }, + start: function() { + this.desktop._serverpush = this; + this.active = true; + this._send(); + }, + stop: function() { + this.desktop._serverpush = null; + this.active = false; + if (this._req) { + this._req.abort(); + this._req = null; + } + } + }); +})(); 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 new file mode 100644 index 0000000000..e1579d8edc --- /dev/null +++ b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/zk.wpd @@ -0,0 +1,4 @@ + + +