001: /* PollingServerPush.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Fri Aug 3 18:53:21 2007, Created by tomyeh
010: }}IS_NOTE
011:
012: Copyright (C) 2007 Potix Corporation. All Rights Reserved.
013:
014: {{IS_RIGHT
015: This program is distributed under GPL Version 2.0 in the hope that
016: it will be useful, but WITHOUT ANY WARRANTY.
017: }}IS_RIGHT
018: */
019: package org.zkoss.zkex.ui.impl;
020:
021: import java.util.List;
022: import java.util.LinkedList;
023: import java.util.Iterator;
024:
025: import org.zkoss.lang.D;
026: import org.zkoss.lang.Threads;
027: import org.zkoss.util.logging.Log;
028:
029: import org.zkoss.zk.ui.Executions;
030: import org.zkoss.zk.ui.Desktop;
031: import org.zkoss.zk.ui.UiException;
032: import org.zkoss.zk.ui.DesktopUnavailableException;
033: import org.zkoss.zk.ui.sys.ServerPush;
034: import org.zkoss.zk.ui.impl.ExecutionCarryOver;
035: import org.zkoss.zk.ui.util.Configuration;
036: import org.zkoss.zk.ui.util.Clients;
037: import org.zkoss.zk.ui.event.Events;
038: import org.zkoss.zk.au.out.AuScript;
039:
040: /**
041: * A server-push implementation that is based on client-polling.
042: *
043: * @author tomyeh
044: * @since 3.0.0
045: */
046: public class PollingServerPush implements ServerPush {
047: private static final Log log = Log.lookup(PollingServerPush.class);
048: /** Denote a server-push thread gives up the activation (timeout). */
049: private static final int GIVEUP = -99;
050:
051: private Desktop _desktop;
052: private Configuration _config;
053: /** List of ThreadInfo. */
054: private final List _pending = new LinkedList();
055: /** The active thread. */
056: private ThreadInfo _active;
057: /** The info to carray over from onPiggyback to the server-push thread. */
058: private ExecutionCarryOver _carryOver;
059: /** A mutex that is used by this object to wait for the server-push thread
060: * to complete.
061: */
062: private final Object _mutex = new Object();
063:
064: /** Returns the JavaScript codes to enable (aka., start) the server push.
065: */
066: protected String getStartScript() {
067: final String start = _config.getPreference(
068: "PollingServerPush.start", null);
069: if (start != null)
070: return start;
071: final StringBuffer sb = new StringBuffer(128).append(
072: "zk.invoke('zkex.ui.cpsp',function(){zkCpsp.start('")
073: .append(_desktop.getId()).append('\'');
074:
075: final int v1 = getIntPref("PollingServerPush.delay.min"), v2 = getIntPref("PollingServerPush.delay.max");
076: if (v1 > 0 && v2 > 0)
077: sb.append(',').append(v1).append(',').append(v2);
078:
079: return sb.append(");});").toString();
080: }
081:
082: private int getIntPref(String key) {
083: final String s = _config.getPreference(key, null);
084: if (s != null) {
085: try {
086: return Integer.parseInt(s);
087: } catch (NumberFormatException ex) {
088: log.warning("Not a number specified at " + key);
089: }
090: }
091: return -1;
092: }
093:
094: /** Returns the JavaScript codes to disable (aka., stop) the server push.
095: */
096: protected String getStopScript() {
097: final String stop = _config.getPreference(
098: "PollingServerPush.stop", null);
099: return stop != null ? stop : "zkCpsp.stop('" + _desktop.getId()
100: + "');";
101: }
102:
103: //ServerPush//
104: public void start(Desktop desktop) {
105: if (_desktop != null)
106: throw new IllegalStateException("Already started");
107:
108: _desktop = desktop;
109: _config = _desktop.getWebApp().getConfiguration();
110: Clients.response(new AuScript(null, getStartScript()));
111: }
112:
113: public void stop() {
114: if (_desktop == null)
115: throw new IllegalStateException("Not started");
116:
117: if (Executions.getCurrent() != null) //Bug 1815480: don't send if timeout
118: Clients.response(new AuScript(null, getStopScript()));
119:
120: _desktop = null; //to cause DesktopUnavailableException being thrown
121: _config = null;
122:
123: synchronized (_pending) {
124: for (Iterator it = _pending.iterator(); it.hasNext();) {
125: final ThreadInfo info = (ThreadInfo) it.next();
126: synchronized (info) {
127: info.notify();
128: }
129: }
130: _pending.clear();
131: }
132: }
133:
134: /** Sets the delay between each polling request.
135: * <p>Default: use the preference called
136: * <code>PollingServerPush.delay.min</code>
137: * <code>PollingServerPush.delay.max</code>,
138: * and <code>PollingServerPush.delay.factor</code>.
139: * If not defined, min is 1100, max is 10000, and factor is 5.
140: */
141: public void setDelay(int min, int max, int factor) {
142: Clients.response(new AuScript(null, "zkau.setSPushInfo('"
143: + _desktop.getId() + "',{min:" + min + ",max:" + max
144: + ",factor:" + factor + "})"));
145: }
146:
147: public void onPiggyback() {
148: long tmexpired = 0;
149: for (int cnt = 0; !_pending.isEmpty();) {
150: //Don't hold the client too long.
151: //In addition, an ill-written code might activate again
152: //before onPiggyback returns. It causes dead-loop in this case.
153: if (tmexpired == 0) { //first time
154: tmexpired = System.currentTimeMillis()
155: + (_config.getMaxProcessTime() >> 1);
156: cnt = _pending.size() + 3;
157: } else if (--cnt < 0
158: || System.currentTimeMillis() > tmexpired) {
159: break;
160: }
161:
162: final ThreadInfo info;
163: synchronized (_pending) {
164: if (_pending.isEmpty())
165: return; //nothing to do
166: info = (ThreadInfo) _pending.remove(0);
167: }
168:
169: synchronized (_mutex) {
170: _carryOver = new ExecutionCarryOver(_desktop);
171:
172: synchronized (info) {
173: if (info.nActive == GIVEUP)
174: continue; //give up and try next
175: info.nActive = 1; //granted
176: info.notify();
177: }
178:
179: if (_desktop == null) //just in case
180: break;
181:
182: try {
183: _mutex.wait(); //wait until the server push is done
184: } catch (InterruptedException ex) {
185: throw UiException.Aide.wrap(ex);
186: }
187: }
188: }
189: }
190:
191: public boolean activate(long timeout) throws InterruptedException,
192: DesktopUnavailableException {
193: if (D.ON && Events.inEventListener())
194: throw new IllegalStateException(
195: "No need to activate in the event listener");
196:
197: final Thread curr = Thread.currentThread();
198: if (_active != null && _active.thread.equals(curr)) { //re-activate
199: ++_active.nActive;
200: return true;
201: }
202:
203: final ThreadInfo info = new ThreadInfo(curr);
204: synchronized (_pending) {
205: if (_desktop != null)
206: _pending.add(info);
207: }
208:
209: synchronized (info) {
210: if (_desktop != null) {
211: info.wait(timeout);
212:
213: if (info.nActive <= 0) { //not granted (timeout)
214: info.nActive = GIVEUP; //denote timeout (and give up)
215: synchronized (_pending) { //undo pending
216: _pending.remove(info);
217: }
218: return false; //timeout
219: }
220: }
221: }
222:
223: if (_desktop == null)
224: throw new DesktopUnavailableException("Stopped");
225:
226: _carryOver.carryOver();
227: _active = info;
228: return true;
229:
230: //Note: we don't mimic inEventListener since 1) ZK doesn't assume it
231: //2) Window depends on it
232: }
233:
234: public void deactivate() {
235: if (_active != null
236: && Thread.currentThread().equals(_active.thread)) {
237: if (--_active.nActive <= 0) {
238: _carryOver.cleanup();
239: _carryOver = null;
240: _active.nActive = 0; //just in case
241: _active = null;
242:
243: //wake up onPiggyback
244: synchronized (_mutex) {
245: _mutex.notify();
246: }
247:
248: Threads.sleep(50);
249: //to minimize the chance that the server-push thread
250: //activate again, before onPiggback polls next _pending
251: }
252: }
253: }
254:
255: /** The info of a server-push thread.
256: * It is also a mutex used to start a pending server-push thread.
257: */
258: private static class ThreadInfo {
259: private final Thread thread;
260: /** # of activate() was called. */
261: private int nActive;
262:
263: private ThreadInfo(Thread thread) {
264: this .thread = thread;
265: }
266:
267: public String toString() {
268: return "[" + thread + ',' + nActive + ']';
269: }
270: }
271: }
|