zk/src/main/java/org/zkoss/zk/ui/impl/DesktopImpl.java
/* DesktopImpl.java
Purpose:
Description:
History:
Wed Jun 22 09:50:57 2005, Created by tomyeh
Copyright (C) 2005 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui.impl;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.io.Serializables;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.lang.SystemException;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.util.CacheMap;
import org.zkoss.util.media.Media;
import org.zkoss.util.resource.Labels;
import org.zkoss.web.servlet.http.Encodes;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.au.AuService;
import org.zkoss.zk.au.out.AuBookmark;
import org.zkoss.zk.au.out.AuClientInfo;
import org.zkoss.zk.au.out.AuHistoryState;
import org.zkoss.zk.device.Device;
import org.zkoss.zk.device.DeviceNotFoundException;
import org.zkoss.zk.device.Devices;
import org.zkoss.zk.mesg.MZk;
import org.zkoss.zk.ui.AbstractComponent;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.ComponentNotFoundException;
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.Page;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.Sessions;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.event.BookmarkEvent;
import org.zkoss.zk.ui.event.ClientInfoEvent;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.HistoryPopStateEvent;
import org.zkoss.zk.ui.event.ScriptErrorEvent;
import org.zkoss.zk.ui.event.VisibilityChangeEvent;
import org.zkoss.zk.ui.ext.ScopeListener;
import org.zkoss.zk.ui.ext.render.DynamicMedia;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.ui.sys.DesktopCache;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zk.ui.sys.EventProcessingThread;
import org.zkoss.zk.ui.sys.ExecutionCtrl;
import org.zkoss.zk.ui.sys.ExecutionsCtrl;
import org.zkoss.zk.ui.sys.IdGenerator;
import org.zkoss.zk.ui.sys.PageCtrl;
import org.zkoss.zk.ui.sys.RequestQueue;
import org.zkoss.zk.ui.sys.Scheduler;
import org.zkoss.zk.ui.sys.ServerPush;
import org.zkoss.zk.ui.sys.SessionCtrl;
import org.zkoss.zk.ui.sys.Storage;
import org.zkoss.zk.ui.sys.UiEngine;
import org.zkoss.zk.ui.sys.Visualizer;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.Configuration;
import org.zkoss.zk.ui.util.DesktopActivationListener;
import org.zkoss.zk.ui.util.DesktopCleanup;
import org.zkoss.zk.ui.util.DesktopSerializationListener;
import org.zkoss.zk.ui.util.EventInterceptor;
import org.zkoss.zk.ui.util.ExecutionCleanup;
import org.zkoss.zk.ui.util.ExecutionInit;
import org.zkoss.zk.ui.util.ExecutionMonitor;
import org.zkoss.zk.ui.util.Monitor;
import org.zkoss.zk.ui.util.UiLifeCycle;
/**
* The implementation of {@link Desktop}.
*
* <p>Note: though {@link DesktopImpl} is serializable, it is designed
* to work with Web container to enable the serialization of sessions.
* It is not suggested to serialize and deserialize it directly since
* many fields might be lost.
*
* <p>On the other hand, it is OK to serialize and deserialize
* {@link Component}.
*
* @author tomyeh
*/
public class DesktopImpl implements Desktop, DesktopCtrl, java.io.Serializable {
private static final Logger log = LoggerFactory.getLogger(DesktopImpl.class);
private static final long serialVersionUID = 20101123L;
/** Represents media stored with {@link #getDownloadMediaURI}.
* It must be distinguishable from component's ID.
*/
private static final String DOWNLOAD_PREFIX = "dwnmed-";
/** A session attribute holding the number of server pushes.
*/
private static final String ATTR_PUSH_COUNT = "org.zkoss.zk.ui.pushes.count";
/** A special event for scheduling a task for server push.
*/
private static final String ON_SCHEDULE = "onSchedule";
/** Represents a file name (it needs to be encoded separately) holder with {@link #getDownloadMediaURI}.
*/
private static final String FILENAME_HOLDER = "$zk_fn$";
private transient WebApp _wapp;
private transient Session _sess;
private String _id;
/** The current directory of this desktop. */
private String _dir = "";
/** The path of the request that causes this desktop to be created. */
private final String _path;
/** The query string. */
private final String _qs;
/** The URI to access the update engine. */
private final String _updateURI;
/** The URI to access the resource engine. */
private final String _resourceURI;
/** List<Page>. */
private final List<Page> _pages = new LinkedList<Page>();
/** Map (String uuid, Component comp). */
private transient Map<String, Component> _comps;
/** A map of attributes. */
private transient SimpleScope _attrs;
//don't create it dynamically because PageImp._ip bind it at constructor
private transient Execution _exec;
// ZK-4194
private final AtomicInteger _scheduleInfoReadCount = new AtomicInteger(0);
// ZK-4194
private final AtomicInteger _scheduleInfoWriteCount = new AtomicInteger(0);
/* ZK-4194: This cache is to avoid a disconnected desktop (no connected and corresponding browser tab)
from stacking ScheduleInfo sent by a server push EventQueue. Cache works as a Map which is not an ordered collection,
we use read/write count to serve as an index and ensure we retrieve it in insertion order. ScheduleInfo cache must be thread safe. */
private final Cache<Integer, ScheduleInfo<? extends Event>> _schedInfos = CacheBuilder.newBuilder()
.maximumSize(Library.getIntProperty(
"org.zkoss.zk.ui.Desktop.maxScheduleInfoSize", 10_000))
.expireAfterWrite(Library.getIntProperty(
"org.zkoss.zk.ui.Desktop.scheduleInfoTimeout", 10),
TimeUnit.MINUTES).build();
/** For handling scheduled task in onSchedule. */
private Component _dummyTarget = null;
/** Next available key. */
private int _nextKey;
/** Next available UUID. */
private int _nextUuid;
/** A special prefix to UUID generated by this desktop.
* It is used to avoid ID conflicts with other desktops in the same
* session.
* Since UUID is long enough plus this prefix, the chance to conflict
* is almost impossible.
*/
private String _uuidPrefix;
/** The request queue. */
private transient RequestQueue _rque;
private String _bookmark = "";
/** The device type. */
private String _devType = "ajax";
/** The device. */
private transient Device _dev; //it will re-init each time getDevice called
/** A map of media (String key, Media content). */
private CacheMap<String, Media> _meds;
/** ID used to identify what is stored in _meds. */
private int _medId;
/** The server push controller, or null if not enabled. */
private transient ServerPush _spush;
/** A temporary object being deserialized but not yet activate. */
private transient ServerPush _spushTemp;
/** The event interceptors. */
private final EventInterceptors _eis = new EventInterceptors();
private transient List<DesktopCleanup> _dtCleans;
private transient List<ExecutionInit> _execInits;
private transient List<ExecutionCleanup> _execCleans;
private transient List<UiLifeCycle> _uiCycles;
private transient List<AuService> _ausvcs;
private transient Visualizer _uv;
private transient Object _uvLock;
private final Lock _storageLock = new ReentrantLock(); // since ZK 8.0.0
private transient ReqResult _lastRes;
private transient List<AuResponse> _piggyRes;
/** A set of keys that shall be generated to the client only once per desktop. */
private transient Set<String> _clientPerDesktops;
private static final int MAX_RESPONSE_ID = 999;
/** The response sequence ID. */
private int _resId; //so next will be 1
/** Whether any onPiggyback listener is registered. */
private boolean _piggybackListened;
/** Whether the server push shall stop after deactivate. */
private boolean _spushShallStop;
/** references of enabling DesktopEventQueues to enable reference counting, when several queues run against one desktop. */
private Set<Serializable> enablers = Collections.synchronizedSet(new HashSet<Serializable>());
/**
* @param wapp the web application
* @param updateURI the URI to access the update engine (no expression allowed).
* Note: it is NOT encoded yet.
* @param path the path that causes this desktop to create.
* If null or empty is specified, it means not available.
* @param deviceType the device type.
* If null or empty is specified, "ajax" is assumed.
* @param request the request (HttpServletRequest if HTTP)
* @since 3.0.1
*/
public DesktopImpl(WebApp wapp, String updateURI, String path, String deviceType, Object request) {
this(wapp, updateURI, updateURI, path, deviceType, request);
}
/**
* @param wapp the web application
* @param updateURI the URI to access the update engine (no expression allowed).
* Note: it is NOT encoded yet.
* @param resourceURI the URI to access the resource engine (no expression allowed).
* Note: it is NOT encoded yet.
* @param path the path that causes this desktop to create.
* If null or empty is specified, it means not available.
* @param deviceType the device type.
* If null or empty is specified, "ajax" is assumed.
* @param request the request (HttpServletRequest if HTTP)
* @since 9.5.0
*/
public DesktopImpl(WebApp wapp, String updateURI, String resourceURI, String path, String deviceType, Object request) {
if (updateURI == null || wapp == null)
throw new IllegalArgumentException("null");
//Feature 1811241: we create a temporary exec (in WebManager.newDesktop),
//so DesktopInit can access Executions.getCurrent
final Execution exec = Executions.getCurrent();
if (exec != null)
((ExecutionCtrl) exec).setDesktop(this);
_wapp = wapp;
_updateURI = updateURI;
_resourceURI = resourceURI;
init();
_sess = Sessions.getCurrent(); //must be the current session
String dir = null;
if (path != null) {
_path = path;
final int j = path.lastIndexOf('/');
if (j >= 0)
dir = path.substring(0, j + 1);
} else {
_path = "";
}
setCurrentDirectory(dir);
_qs = getQueryString(request);
if (deviceType != null && deviceType.length() != 0)
setDeviceType(deviceType);
final Configuration config = _wapp.getConfiguration();
_exec = exec; //fake
try {
final WebAppCtrl wappc = (WebAppCtrl) _wapp;
final DesktopCache dc = _sess != null ? wappc.getDesktopCache(_sess) : null;
//_sess is null if in a working thread
final IdGenerator idgen = wappc.getIdGenerator();
if (idgen != null)
_id = idgen.nextDesktopId(this);
if (_id == null)
_id = nextDesktopId(dc);
else if (idgen != null)
ComponentsCtrl.checkUuid(_id);
updateUuidPrefix();
resetLabels();
config.invokeDesktopInits(this, request); //it might throw exception
if (exec != null && exec.isVoided())
return; //sendredirect or forward
if (dc != null)
dc.addDesktop(this); //add to cache after invokeDesktopInits
final Monitor monitor = config.getMonitor();
if (monitor != null) {
try {
monitor.desktopCreated(this);
} catch (Throwable ex) {
log.error("", ex);
}
}
} finally {
_exec = null;
}
}
private static String getQueryString(Object request) {
try {
if (request instanceof javax.servlet.http.HttpServletRequest)
return ((javax.servlet.http.HttpServletRequest) request).getQueryString();
} catch (Throwable ex) { //ignore any error (such as no servlet at all)
}
return null;
}
public String getQueryString() {
return _qs;
}
private static final String DESKTOP_ID_PREFIX = "z_";
private static class Holder {
static final SecureRandom PRNG = new SecureRandom();
static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();
}
private static String nextDesktopId(DesktopCache dc) {
String dtid;
do {
dtid = generateDesktopId();
} while (dc != null && dc.getDesktopIfAny(dtid) != null);
return dtid;
}
private static String generateDesktopId() {
final byte[] randomBytesValue = new byte[16];
Holder.PRNG.nextBytes(randomBytesValue);
return new StringBuilder(32)
.append(DESKTOP_ID_PREFIX)
.append(Holder.ENCODER.encodeToString(randomBytesValue))
.toString();
}
/** Initialization for constructor and de-serialized. */
private void init() {
_uvLock = new Object();
_rque = newRequestQueue();
_comps = new HashMap<String, Component>(64);
_attrs = new SynchronizedScope(this);
//Use thread-safe scope, Since some class might access asynchronous,
//i.e., without locking. Example, AuUploader
}
/** Updates _uuidPrefix based on _id. */
private void updateUuidPrefix() {
final StringBuffer sb = new StringBuffer();
int val = _id.hashCode() + (int) System.currentTimeMillis();
//Thus, the number will 0, 1... max, 0, 1..., max, 0, 1 (less conflict)
if (val < 0 && (val += Integer.MIN_VALUE) < 0)
val = -val; //impossible but just in case
//Note: ComponentsCtrl.isAutoUuid assumes
//0: lower, 1: digit or upper, 2: letter or digit, 3: upper
int v = (val % 26) + 36;
val /= 26;
sb.append(toLetter(v));
v = val % 36;
val /= 36;
sb.append(toLetter(v));
v = val % 62;
val /= 62;
sb.append(toLetter(v));
_uuidPrefix = sb.append(toLetter((val % 26) + 10)).toString();
}
private static final char toLetter(int v) {
if (v < 10) {
return (char) ('0' + v);
} else if (v < 36) {
return (char) (v + ('A' - 10));
} else {
return (char) (v + ('a' - 36));
}
}
private void resetLabels() {
if (!Boolean.valueOf(Library.getProperty("org.zkoss.util.label.cache", "true")))
Labels.reset();
}
public String getId() {
return _id;
}
/** Creates the request queue.
* It is called when the desktop is initialized.
*
* <p>You may override it to provide your implementation of
* {@link RequestQueue} to control how to optimize the AU requests.
*
* <p>Default: creates an instance from {@link RequestQueueImpl};
*
* @since 2.4.0
*/
protected RequestQueue newRequestQueue() {
return new RequestQueueImpl();
}
//-- Desktop --//
public String getDeviceType() {
return _devType;
}
public Device getDevice() {
if (_dev == null)
_dev = Devices.getDevice(_devType);
return _dev;
}
public void setDeviceType(String deviceType) {
//Note: we check _comps.isEmpty() only if device type differs, because
//a desktop might have several richlet and each of them will call
//this method once
if (!_devType.equals(deviceType)) {
if (deviceType == null || deviceType.length() == 0)
throw new IllegalArgumentException("empty");
if (!Devices.exists(deviceType))
throw new DeviceNotFoundException(deviceType, MZk.NOT_FOUND, deviceType);
if (!_comps.isEmpty())
throw new UiException("Unable to change the device type since some components are attached.");
_devType = deviceType;
_dev = null;
if (_sess != null) //not in a working thread
((SessionCtrl) _sess).setDeviceType(_devType);
}
}
public Execution getExecution() {
return _exec;
}
public final Session getSession() {
return _sess;
}
public String getUpdateURI(String pathInfo) {
final String uri;
if (pathInfo == null || pathInfo.length() == 0) {
uri = _updateURI;
} else {
if (pathInfo.charAt(0) != '/')
pathInfo = '/' + pathInfo;
uri = _updateURI + pathInfo;
}
return _exec.encodeURL(uri);
}
public String getResourceURI(String pathInfo) {
final String uri;
if (pathInfo == null || pathInfo.length() == 0) {
uri = _resourceURI;
} else {
if (pathInfo.charAt(0) != '/')
pathInfo = '/' + pathInfo;
uri = _resourceURI + pathInfo;
}
return _exec.encodeURL(uri);
}
public String getDynamicMediaURI(Component comp, String pathInfo) {
if (!(((ComponentCtrl) comp).getExtraCtrl() instanceof DynamicMedia))
throw new UiException(DynamicMedia.class + " not implemented by getExtraCtrl() of " + comp);
final StringBuffer sb = new StringBuffer(64).append("/view/").append(getId()).append('/').append(comp.getUuid())
.append('/');
Strings.encode(sb, System.identityHashCode(comp) & 0xffff);
if (pathInfo != null && pathInfo.length() > 0) {
if (pathInfo.charAt(0) != '/')
sb.append('/');
sb.append(pathInfo);
}
return getUpdateURI(sb.toString());
}
public String getDownloadMediaURI(Media media, String pathInfo) {
if (media == null)
throw new IllegalArgumentException("null media");
if (_meds == null) {
_meds = new CacheMap<String, Media>();
_meds.setMaxSize(500);
_meds.setLifetime(15 * 60 * 1000);
//15 minutes (CONSIDER: configurable)
} else {
housekeep();
}
String medId = Strings.encode(new StringBuffer(12).append(DOWNLOAD_PREFIX), _medId++).toString();
_meds.put(medId, media);
final StringBuffer sb = new StringBuffer(64).append("/view/").append(getId()).append('/').append(medId)
.append('/');
Strings.encode(sb, System.identityHashCode(media) & 0xffff);
if (pathInfo != null && pathInfo.length() > 0) {
sb.append('/').append(FILENAME_HOLDER);
if (pathInfo.charAt(0) == '/')
pathInfo = pathInfo.substring(1);
String uri = getUpdateURI(sb.toString());
int holderPos = uri.lastIndexOf(FILENAME_HOLDER);
return uri.substring(0, holderPos)
+ encodeFilename(pathInfo)
+ uri.substring(holderPos + FILENAME_HOLDER.length());
}
return getUpdateURI(sb.toString());
}
// ZK-3809: # is valid in URL as a reference, but it is invalid as a filename
private String encodeFilename(String filename) {
if (filename == null)
return "";
try {
return Encodes.encodeURIComponent(filename);
} catch (UnsupportedEncodingException ex) {
throw new SystemException(ex);
}
}
public Media getDownloadMedia(String medId, boolean reserved) {
return _meds != null ? _meds.get(medId) : null;
}
/** Cleans up redundant data. */
private void housekeep() {
if (_meds != null)
_meds.expunge();
}
public Page getPage(String pageId) {
//Spec: we allow user to access this method concurrently
final Page page = getPageIfAny(pageId);
if (page == null)
throw new ComponentNotFoundException("Page not found: " + pageId);
return page;
}
public Page getPageIfAny(String pageId) {
//Spec: we allow user to access this method concurrently, so
//synchronized is required
Page page = null;
synchronized (_pages) {
for (Page pg : _pages) {
if (Objects.equals(pageId, pg.getId()))
return pg;
if (Objects.equals(pageId, pg.getUuid()))
page = pg;
}
}
return page;
}
public boolean hasPage(String pageId) {
return getPageIfAny(pageId) != null;
}
public Collection<Page> getPages() {
//No synchronized is required because it cannot be access concurrently
return Collections.unmodifiableCollection(_pages);
}
public Page getFirstPage() {
return _pages.isEmpty() ? null : _pages.get(0);
}
public String getBookmark() {
return _bookmark;
//Notice: since the bookmark (#xx) not sent from the HTTP request,
//we can only assume "" when the page is loading
}
public void setBookmark(String name) {
setBookmark(name, false);
}
public void setBookmark(String name, boolean replace) {
if (_exec == null)
throw new IllegalStateException("Not the current desktop: " + this);
//B50-ZK-58: question mark is legal char
//if (name.indexOf('#') >= 0 || name.indexOf('?') >= 0)
// throw new IllegalArgumentException("Illegal character: # ?");
_bookmark = name;
addResponse(new AuBookmark(name, replace));
}
private void addResponse(AuResponse response) {
((WebAppCtrl) _wapp).getUiEngine().addResponse(response);
}
public Collection<Component> getComponents() {
return _comps.values();
}
public Component getComponentByUuid(String uuid) {
final Component comp = _comps.get(uuid);
if (comp == null)
throw new ComponentNotFoundException("Component not found: " + uuid);
return comp;
}
public Component getComponentByUuidIfAny(String uuid) {
return _comps.get(uuid);
}
public void addComponent(Component comp) {
//to avoid misuse, check whether new comp belongs to the same device type
final LanguageDefinition langdef = comp.getDefinition().getLanguageDefinition();
if (langdef != null && !_devType.equals(langdef.getDeviceType()))
throw new UiException(
"Component, " + comp + ", does not belong to the same device type of the desktop, " + _devType);
final String uuid = comp.getUuid();
final Component old = _comps.put(uuid, comp);
if (old != comp && old != null) {
_comps.put(uuid, old); //recover
throw new InternalError("Caller shall prevent it: Register a component twice: " + comp);
} /* For performance reason, we don't check if a component is
detached and attached back (in another execution). Rather, reset
_uuid when it is recycled (refer to AbstractComponent.setPage0
(the caller of removeComponent has to reset)
else if (_uuidRecycle != null && !_uuidRecycle.isEmpty()) {
for (RecycleInfo ri: _uuidRecycle) {
final List<String> uuids = ri.uuids;
if (uuids.remove(uuid)) {
if (uuids.isEmpty())
it.remove();
break;
}
}
}*/
}
public boolean removeComponent(Component comp) {
final String uuid = comp.getUuid();
_comps.remove(uuid);
return false; // always since 10.0.0 for ZK-5049: Deprecate org.zkoss.zk.ui.uuidRecycle.disable
}
public Component mapComponent(String uuid, Component comp) {
if (uuid == null)
throw new IllegalArgumentException("null");
return comp != null ? _comps.put(uuid, comp) : _comps.remove(uuid);
//no recycle, no check
}
private static int getExecId() {
final Execution exec = Executions.getCurrent();
return exec != null ? System.identityHashCode(exec) : 0;
}
public Map<String, Object> getAttributes() {
return _attrs.getAttributes();
}
public Object getAttribute(String name) {
return _attrs.getAttribute(name);
}
public Object getAttribute(String name, boolean recurse) {
Object val = getAttribute(name);
if (val != null || !recurse || hasAttribute(name))
return val;
if (_sess != null)
return _sess.getAttribute(name, true);
if (_wapp != null)
return _wapp.getAttribute(name, true);
return null;
}
public boolean hasAttribute(String name) {
return _attrs.hasAttribute(name);
}
public boolean hasAttribute(String name, boolean recurse) {
if (hasAttribute(name))
return true;
if (recurse) {
if (_sess != null)
return _sess.hasAttribute(name, true);
if (_wapp != null)
return _wapp.hasAttribute(name, true);
}
return false;
}
public Object setAttribute(String name, Object value) {
return _attrs.setAttribute(name, value);
}
public Object setAttribute(String name, Object value, boolean recurse) {
if (recurse && !hasAttribute(name)) {
if (_sess != null) {
if (_sess.hasAttribute(name, true))
return _sess.setAttribute(name, value, true);
} else if (_wapp != null) {
if (_wapp.hasAttribute(name, true))
return _wapp.setAttribute(name, value, true);
}
}
return setAttribute(name, value);
}
public Object removeAttribute(String name) {
return _attrs.removeAttribute(name);
}
public Object removeAttribute(String name, boolean recurse) {
if (recurse && !hasAttribute(name)) {
if (_sess != null) {
if (_sess.hasAttribute(name, true))
return _sess.removeAttribute(name, true);
} else if (_wapp != null) {
if (_wapp.hasAttribute(name, true))
return _wapp.removeAttribute(name, true);
}
return null;
}
return removeAttribute(name);
}
public boolean addScopeListener(ScopeListener listener) {
return _attrs.addScopeListener(listener);
}
public boolean removeScopeListener(ScopeListener listener) {
return _attrs.removeScopeListener(listener);
}
public WebApp getWebApp() {
return _wapp;
}
public String getRequestPath() {
return _path;
}
public String getCurrentDirectory() {
return _dir;
}
public void setCurrentDirectory(String dir) {
if (dir == null) {
dir = "";
} else {
final int len = dir.length() - 1;
if (len >= 0 && dir.charAt(len) != '/')
dir += '/';
}
_dir = dir;
}
//-- DesktopCtrl --//
public RequestQueue getRequestQueue() {
housekeep();
return _rque;
}
public void setExecution(Execution exec) {
_exec = exec;
}
public void service(AuRequest request, boolean everError) {
if (_ausvcs != null) {
//Note: removeListener might be called when invoking svc.service()
for (Iterator<AuService> it = new LinkedList<AuService>(_ausvcs).iterator(); it.hasNext();)
if (it.next().service(request, everError))
return;
}
final Component comp = request.getComponent();
if (comp != null) {
final AuService svc = comp.getAuService();
if ((svc == null || !svc.service(request, everError)) && comp.getDesktop() != null)
((ComponentCtrl) comp).service(request, everError);
return; //done (it's comp's job to handle it)
}
final String cmd = request.getCommand();
if (Events.ON_BOOKMARK_CHANGE.equals(cmd)) {
BookmarkEvent evt = BookmarkEvent.getBookmarkEvent(request);
_bookmark = evt.getBookmark();
Events.postEvent(evt);
} else if (Events.ON_CLIENT_INFO.equals(cmd)) {
Events.postEvent(ClientInfoEvent.getClientInfoEvent(request));
} else if (Events.ON_VISIBILITY_CHANGE.equals(cmd)) {
Events.postEvent(VisibilityChangeEvent.getVisibilityChangeEvent(request));
} else if (Events.ON_HISTORY_POP_STATE.equals(cmd)) {
Events.postEvent(HistoryPopStateEvent.getHistoryPopStateEvent(request));
} else if (Events.ON_SCRIPT_ERROR.equals(cmd)) {
Events.postEvent(ScriptErrorEvent.getScriptErrorEvent(request));
} else if ("rmDesktop".equals(cmd)) {
((WebAppCtrl) request.getDesktop().getWebApp()).getUiEngine()
.setAbortingReason(new org.zkoss.zk.ui.impl.AbortByRemoveDesktop());
//to avoid surprise, we don't remove it now
//rather, it is done by AbortByRemoveDesktop.getResponse
} else if ("redraw".equals(cmd)) {
invalidate();
} else if ("error".equals(cmd)) {
final Map<String, Object> data = request.getData();
if (data != null)
log.error("Client encountered an error at {}:\n{}", data.get("href"), data.get("message"));
} else if ("fallbackServerPushClass".equals(cmd)) {
try {
getDevice().setServerPushClass(Classes.forNameByThread("org.zkoss.zkex.ui.comet.CometServerPush"));
} catch (ClassNotFoundException e) {
throw new UiException("Class not found: org.zkoss.zkex.ui.comet.CometServerPush");
}
} else
Events.postEvent(Event.getEvent(request));
}
public Visualizer getVisualizer() {
return _uv;
}
public void setVisualizer(Visualizer uv) {
_uv = uv;
}
public Object getActivationLock() {
return _uvLock;
}
public int getNextKey() {
return _nextKey++;
}
public String getNextUuid(Page page) {
final IdGenerator idgen = ((WebAppCtrl) _wapp).getIdGenerator();
String uuid = idgen != null ? idgen.nextPageUuid(page) : null;
if (uuid == null)
return nextUuid();
ComponentsCtrl.checkUuid(uuid);
return uuid;
}
public String getNextUuid(Component comp) {
final IdGenerator idgen = ((WebAppCtrl) _wapp).getIdGenerator();
String uuid = null;
if (idgen != null) {
try {
uuid = idgen.nextComponentUuid(this, comp, Utils.getComponentInfo(comp));
} catch (AbstractMethodError ex) {
try {
Method method = idgen.getClass().getMethod("nextComponentUuid", Desktop.class, Component.class);
Fields.setAccessible(method, true);
uuid = (String) method.invoke(idgen, this, comp);
} catch (Exception e) {
throw UiException.Aide.wrap(e);
}
}
}
if (uuid == null)
return nextUuid();
ComponentsCtrl.checkUuid(uuid);
return uuid;
}
private String nextUuid() {
return ComponentsCtrl.toAutoId(_uuidPrefix, _nextUuid++);
}
public void addPage(Page page) {
//We have to synchronize it due to getPage allows concurrent access
synchronized (_pages) {
_pages.add(page);
if (log.isDebugEnabled())
log.debug("After added, pages: {}", _pages);
}
afterPageAttached(page, this);
_wapp.getConfiguration().afterPageAttached(page, this);
}
public void removePage(Page page) {
synchronized (_pages) {
if (!_pages.remove(page))
return;
//Both UiVisualizer.getResponses and Include.setChildPage
//might call removePage
if (log.isDebugEnabled())
log.debug("After removed, pages: {}", _pages);
}
removeComponents(page.getRoots());
// should be before afterPageDetached to access binder information
beforeDestroyPage(page, _wapp.getConfiguration());
afterPageDetached(page, this);
_wapp.getConfiguration().afterPageDetached(page, this);
((PageCtrl) page).destroy();
}
private void removeComponents(Collection<Component> comps) {
for (Component comp : comps) {
removeComponents(comp.getChildren()); //recursive
removeComponent(comp);
}
}
private void beforeDestroyPage(Page page, Configuration config) {
// ZK-1148: @Destroy support
for (Component root: page.getRoots()) {
config.invokeCallback("destroy", root);
}
}
public void setId(String id) {
if (!((ExecutionCtrl) _exec).isRecovering())
throw new IllegalStateException("Callable only in recovering");
if (id == null || id.length() <= 1)
throw new IllegalArgumentException(
"Invalid desktop ID. You have to recover to the original value, not creating a new value: " + id);
//_sess and dc are null if in a working thread
final DesktopCache dc = _sess != null ? ((WebAppCtrl) _wapp).getDesktopCache(_sess) : null;
if (dc != null)
dc.removeDesktop(this);
init();
_id = id;
updateUuidPrefix();
if (dc != null)
dc.addDesktop(this);
}
public void recoverDidFail(Throwable ex) {
((WebAppCtrl) _wapp).getDesktopCache(_sess).removeDesktop(this);
}
public void recycle() {
_clientPerDesktops = null; //re-generation is required
}
/** Marks the per-desktop information of the given key will be generated,
* and returns true if the information is not generated yet
* (i.e., this method is NOT called with the given key).
* You could use this method to minimize the bytes to be sent to
* the client if the information is required only once per desktop.
*/
/*package*/ boolean markClientInfoPerDesktop(String key) {
if (_clientPerDesktops == null)
_clientPerDesktops = new HashSet<String>(32);
return _clientPerDesktops.add(key);
}
public boolean isAlive() {
return _rque != null;
}
public void destroy() {
final ExecutionMonitor execmon = _wapp != null //just in case
? _wapp.getConfiguration().getExecutionMonitor() : null;
_rque = null; //denote it is destroyed
final ServerPush sp = _spush; //avoid racing
if (sp != null) {
_spush = null;
try {
sp.stop();
} catch (Throwable ex) {
log.warn("Failed to stop server-push, " + sp, ex);
}
}
try {
final List<Page> pages = new ArrayList<Page>(_pages);
final Configuration config = _wapp == null ? null : _wapp.getConfiguration();
for (Page page : pages) {
try {
beforeDestroyPage(page, config);
((PageCtrl) page).destroy();
} catch (Throwable ex) {
log.warn("Failed to destroy " + page, ex);
}
}
_pages.clear();
} catch (Throwable ex) {
log.warn("Failed to clean up pages of " + this, ex);
}
if (execmon != null)
execmon.desktopDestroy(this);
//theoretically, the following is not necessary, but, to be safe...
_attrs.getAttributes().clear();
_comps = new HashMap<String, Component>(2); //not clear() since # of comps might huge
_meds = null;
//_sess = null; => not sure whether it can be nullify
//_wapp = null; => SimpleDesktopCache.desktopDestroyed depends on it
//Wake up all pending requests (and they will be killed because of !isAlive())
final Object lock = getActivationLock();
if (lock != null) {
synchronized (lock) {
lock.notifyAll();
}
}
}
public Collection<EventProcessingThread> getSuspendedThreads() {
return ((WebAppCtrl) _wapp).getUiEngine().getSuspendedThreads(this);
}
public boolean ceaseSuspendedThread(EventProcessingThread evtthd, String cause) {
return ((WebAppCtrl) _wapp).getUiEngine().ceaseSuspendedThread(this, evtthd, cause);
}
//-- Object --//
public String toString() {
return "[Desktop " + _id + ':' + _path + ']';
}
public void sessionWillPassivate(Session sess) {
Execution exec = Executions.getCurrent();
if (exec != null) { //not possible, but just in case
sessWillPassivate();
} else {
exec = new org.zkoss.zk.ui.impl.PhantomExecution(this);
safeActivate(exec);
try {
sessWillPassivate();
} finally {
safeDeactivate(exec);
}
}
}
public void sessionDidActivate(Session sess) {
_sess = sess;
_wapp = sess.getWebApp();
Execution exec = Executions.getCurrent();
if (exec != null) { //not possible, but just in case
sessDidActivate();
} else {
exec = new org.zkoss.zk.ui.impl.PhantomExecution(this);
safeActivate(exec);
try {
sessDidActivate();
if (_spushTemp != null)
enableServerPush0(_spushTemp, true);
} finally {
safeDeactivate(exec);
_spushTemp = null;
}
}
}
/** Safe to be called even if the Web application has been destroyed
*/
private void safeActivate(Execution exec) {
final UiEngine uieng = ((WebAppCtrl) _wapp).getUiEngine();
if (uieng != null) {
uieng.activate(exec);
} else {
_exec = exec;
ExecutionsCtrl.setCurrent(exec);
}
}
/** Safe to be called even if the Web application has been destroyed
*/
private void safeDeactivate(Execution exec) {
final UiEngine uieng = ((WebAppCtrl) _wapp).getUiEngine();
if (uieng != null) {
uieng.deactivate(exec);
} else {
_exec = null;
ExecutionsCtrl.setCurrent(null);
}
}
private void sessWillPassivate() {
for (Page page : _pages)
((PageCtrl) page).sessionWillPassivate(this);
if (_dev != null)
_dev.sessionWillPassivate(this);
willPassivate(_attrs.getAttributes().values());
willPassivate(_attrs.getListeners());
willPassivate(_dtCleans);
willPassivate(_execInits);
willPassivate(_execCleans);
willPassivate(_uiCycles);
}
private void sessDidActivate() {
if (_dev != null)
_dev.sessionDidActivate(this);
for (Page page : _pages)
((PageCtrl) page).sessionDidActivate(this);
didActivate(_attrs.getAttributes().values());
didActivate(_attrs.getListeners());
didActivate(_dtCleans);
didActivate(_execInits);
didActivate(_execCleans);
didActivate(_uiCycles);
}
private void willPassivate(Collection c) {
if (c != null)
for (Iterator it = c.iterator(); it.hasNext();)
willPassivate(it.next());
}
private void willPassivate(Object o) {
if (o instanceof DesktopActivationListener)
((DesktopActivationListener) o).willPassivate(this);
}
private void didActivate(Collection c) {
if (c != null)
for (Iterator it = c.iterator(); it.hasNext();)
didActivate(it.next());
}
private void didActivate(Object o) {
if (o instanceof DesktopActivationListener)
((DesktopActivationListener) o).didActivate(this);
}
//-- Serializable --//
//NOTE: they must be declared as private
private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
final Map<String, Object> attrs = _attrs.getAttributes();
willSerialize(attrs.values());
Serializables.smartWrite(s, attrs);
final List<ScopeListener> lns = _attrs.getListeners();
willSerialize(lns);
Serializables.smartWrite(s, lns);
willSerialize(_dtCleans);
Serializables.smartWrite(s, _dtCleans);
willSerialize(_execInits);
Serializables.smartWrite(s, _execInits);
willSerialize(_execCleans);
Serializables.smartWrite(s, _execCleans);
willSerialize(_uiCycles);
Serializables.smartWrite(s, _uiCycles);
willSerialize(_ausvcs);
Serializables.smartWrite(s, _ausvcs);
if (_spush == null || _spush instanceof java.io.Serializable || _spush instanceof java.io.Externalizable)
s.writeObject(_spush);
else
s.writeObject(_spush.getClass().getName());
}
private void willSerialize(Collection c) {
if (c != null)
for (Iterator it = c.iterator(); it.hasNext();)
willSerialize(it.next());
}
private void willSerialize(Object o) {
if (o instanceof DesktopSerializationListener)
((DesktopSerializationListener) o).willSerialize(this);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
init();
//get back _comps from _pages
for (Page page : _pages)
for (Component root = page.getFirstRoot(); root != null; root = root.getNextSibling())
addAllComponents(root);
final Map<String, Object> attrs = _attrs.getAttributes();
Serializables.smartRead(s, attrs);
final List<ScopeListener> lns = _attrs.getListeners();
Serializables.smartRead(s, lns);
_dtCleans = Serializables.smartRead(s, _dtCleans);
_execInits = Serializables.smartRead(s, _execInits);
_execCleans = Serializables.smartRead(s, _execCleans);
_uiCycles = Serializables.smartRead(s, _uiCycles);
_ausvcs = Serializables.smartRead(s, _ausvcs);
didDeserialize(attrs.values());
didDeserialize(lns);
didDeserialize(_dtCleans);
didDeserialize(_execInits);
didDeserialize(_execCleans);
didDeserialize(_uiCycles);
didDeserialize(_ausvcs);
Object o = s.readObject();
if (o != null) {
ServerPush sp = null;
if (o instanceof String) {
try {
sp = (ServerPush) Class.forName(
(String) o).getDeclaredConstructor().newInstance();
} catch (Throwable ignored) {
// ignore
}
} else
sp = (ServerPush) o;
_spushTemp = sp;
}
}
private void didDeserialize(Collection c) {
if (c != null)
for (Iterator it = c.iterator(); it.hasNext();)
didDeserialize(it.next());
}
private void didDeserialize(Object o) {
if (o instanceof DesktopSerializationListener)
((DesktopSerializationListener) o).didDeserialize(this);
}
private void addAllComponents(Component comp) {
addComponent(comp);
for (Component child : comp.getChildren())
addAllComponents(child);
}
public void addListener(Object listener) {
boolean added = false;
if (listener instanceof EventInterceptor) {
_eis.addEventInterceptor((EventInterceptor) listener);
added = true;
}
if (listener instanceof DesktopCleanup) {
_dtCleans = addListener0(_dtCleans, (DesktopCleanup) listener);
added = true;
}
if (listener instanceof ExecutionInit) {
_execInits = addListener0(_execInits, (ExecutionInit) listener);
added = true;
}
if (listener instanceof ExecutionCleanup) {
_execCleans = addListener0(_execCleans, (ExecutionCleanup) listener);
added = true;
}
if (listener instanceof UiLifeCycle) {
_uiCycles = addListener0(_uiCycles, (UiLifeCycle) listener);
added = true;
}
if (listener instanceof AuService) {
_ausvcs = addListener0(_ausvcs, (AuService) listener);
added = true;
}
if (!added)
throw new IllegalArgumentException("Unknown listener: " + listener);
}
private <T> List<T> addListener0(List<T> list, T listener) {
if (list == null)
list = new LinkedList<T>();
list.add(listener);
return list;
}
public boolean removeListener(Object listener) {
boolean found = false;
if (listener instanceof EventInterceptor && _eis.removeEventInterceptor((EventInterceptor) listener))
found = true;
if (listener instanceof DesktopCleanup && removeListener0(_dtCleans, listener))
found = true;
if (listener instanceof ExecutionInit && removeListener0(_execInits, listener))
found = true;
if (listener instanceof ExecutionCleanup && removeListener0(_execCleans, listener))
found = true;
if (listener instanceof UiLifeCycle && removeListener0(_uiCycles, listener))
found = true;
if (listener instanceof AuService && removeListener0(_ausvcs, listener))
found = true;
return found;
}
private boolean removeListener0(List list, Object listener) {
//Since 3.0.6: To be consistent with Configuration,
//use equals instead of ==
if (list != null && listener != null)
for (Iterator it = list.iterator(); it.hasNext();) {
if (it.next().equals(listener)) {
it.remove();
return true;
}
}
return false;
}
public Event beforeSendEvent(Event event) {
event = _eis.beforeSendEvent(event);
if (event != null)
event = _wapp.getConfiguration().beforeSendEvent(event);
return event;
}
public Event beforePostEvent(Event event) {
event = _eis.beforePostEvent(event);
if (event != null)
event = _wapp.getConfiguration().beforePostEvent(event);
return event;
}
public Event beforeProcessEvent(Event event) throws Exception {
event = _eis.beforeProcessEvent(event);
if (event != null)
event = _wapp.getConfiguration().beforeProcessEvent(event);
return event;
}
public void afterProcessEvent(Event event) throws Exception {
_eis.afterProcessEvent(event);
_wapp.getConfiguration().afterProcessEvent(event);
if (Events.ON_DESKTOP_RECYCLE.equals(event.getName())) {
if (_bookmark.length() > 0)
addResponse(new AuBookmark(_bookmark));
l_out: for (Page page : _pages)
for (Component root = page.getFirstRoot(); root != null; root = root.getNextSibling())
if (Events.isListened(root, Events.ON_CLIENT_INFO, false)) {
setAttribute("org.zkoss.desktop.clientinfo.enabled", true);
addResponse(new AuClientInfo(this));
break l_out;
}
l_out: for (Page page : _pages)
for (Component root = page.getFirstRoot(); root != null; root = root.getNextSibling())
if (Events.isListened(root, Events.ON_VISIBILITY_CHANGE, false)) {
setAttribute("org.zkoss.desktop.visibilitychange.enabled", true);
break l_out;
}
}
}
public void invokeDesktopCleanups() {
if (_dtCleans != null) {
for (Iterator<DesktopCleanup> it = new LinkedList<DesktopCleanup>(_dtCleans).iterator(); it.hasNext();) {
final DesktopCleanup listener = it.next();
try {
listener.cleanup(this);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
}
}
}
}
public void invokeExecutionInits(Execution exec, Execution parent) throws UiException {
if (_execInits != null) {
for (Iterator<ExecutionInit> it = new LinkedList<ExecutionInit>(_execInits).iterator(); it.hasNext();) {
try {
it.next().init(exec, parent);
} catch (Throwable ex) {
throw UiException.Aide.wrap(ex);
//Don't intercept; to prevent the creation of a session
}
}
}
}
public void invokeExecutionCleanups(Execution exec, Execution parent, List<Throwable> errs) {
if (_execCleans != null) {
for (Iterator<ExecutionCleanup> it = new LinkedList<ExecutionCleanup>(_execCleans).iterator(); it
.hasNext();) {
final ExecutionCleanup listener = it.next();
try {
listener.cleanup(exec, parent, errs);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
if (errs != null)
errs.add(ex);
}
}
}
}
public void afterComponentAttached(Component comp, Page page) {
if (_uiCycles != null) {
for (Iterator<UiLifeCycle> it = new LinkedList<UiLifeCycle>(_uiCycles).iterator(); it.hasNext();) {
final UiLifeCycle listener = it.next();
try {
listener.afterComponentAttached(comp, page);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
}
}
}
}
public void afterComponentDetached(Component comp, Page prevpage) {
if (_uiCycles != null) {
for (Iterator<UiLifeCycle> it = new LinkedList<UiLifeCycle>(_uiCycles).iterator(); it.hasNext();) {
final UiLifeCycle listener = it.next();
try {
listener.afterComponentDetached(comp, prevpage);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
}
}
}
}
public void afterComponentMoved(Component parent, Component child, Component prevparent) {
if (_uiCycles != null) {
for (Iterator<UiLifeCycle> it = new LinkedList<UiLifeCycle>(_uiCycles).iterator(); it.hasNext();) {
final UiLifeCycle listener = it.next();
try {
listener.afterComponentMoved(parent, child, prevparent);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
}
}
}
}
private void afterPageAttached(Page page, Desktop desktop) {
if (_uiCycles != null) {
for (Iterator<UiLifeCycle> it = new LinkedList<UiLifeCycle>(_uiCycles).iterator(); it.hasNext();) {
final UiLifeCycle listener = it.next();
try {
listener.afterPageAttached(page, desktop);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
}
}
}
}
private void afterPageDetached(Page page, Desktop prevdesktop) {
if (_uiCycles != null) {
for (Iterator<UiLifeCycle> it = new LinkedList<UiLifeCycle>(_uiCycles).iterator(); it.hasNext();) {
final UiLifeCycle listener = it.next();
try {
listener.afterPageDetached(page, prevdesktop);
} catch (Throwable ex) {
log.error("Failed to invoke " + listener, ex);
}
}
}
}
//Server Push//
public boolean enableServerPush(boolean enable) {
return enableServerPush(enable, null);
}
public boolean enableServerPush(boolean enable, Serializable enabler) {
return enableServerPush(null, enable, enabler);
}
//ZK-1840 make sure the serverpush does not get enabled/disabled multiple times, to keep reference counting consistent
//and provide possibility to disable/enable in the same execution
private boolean enableServerPush(ServerPush serverPush, boolean enable, Serializable enabler) {
synchronized (enablers) {
boolean enablersEmptyBefore = enablers.isEmpty();
if (enable) {
//handle dummy target
if (_dummyTarget == null) {
_dummyTarget = new AbstractComponent();
_dummyTarget.addEventListener(ON_SCHEDULE, new ScheduleListener());
}
// B65-ZK-2105: Do not add if enabler is null.
if (enabler != null && !enablers.add(enabler)) {
log.debug("trying to enable already enabled serverpush by: " + enabler);
return false;
}
if (enablersEmptyBefore) {
return enableServerPush0(serverPush, enable);
}
} else {
// B65-ZK-2105: Do remove if enabler is null.
if (enabler != null && !enablers.remove(enabler)) {
log.debug("trying to disable already disabled serverpush by: " + enabler);
return false;
}
// B65-ZK-2105: No need to check if enablers is empty before, it would cause B30-2202620 side effect.
if (enablers.isEmpty()) {
return enableServerPush0(serverPush, enable);
}
}
}
//nothing to do already enabled/disabled by this "enabler"
return false;
}
public boolean enableServerPush(ServerPush serverpush) {
final boolean serverPushAlreadyExists = _spush != null, enable = serverpush != null;
if (serverPushAlreadyExists != enable || serverpush != _spush) {
if (serverPushAlreadyExists)
enableServerPush(false, null);
if (enable)
enableServerPush(serverpush, true, null);
}
return serverPushAlreadyExists;
}
private boolean enableServerPush0(ServerPush sp, boolean enable) {
if (_sess == null)
throw new IllegalStateException("Server push cannot be enabled in a working thread");
final boolean serverPushAlreadyExists = _spush != null;
if (serverPushAlreadyExists != enable) {
final Integer icnt = (Integer) _sess.getAttribute(ATTR_PUSH_COUNT);
int cnt = icnt != null ? icnt.intValue() : 0;
if (enable) {
if (Executions.getCurrent() == null)
throw new IllegalStateException("Server Push cannot be started without execution");
final int maxcnt = _wapp.getConfiguration().getSessionMaxPushes();
if (maxcnt >= 0 && cnt >= maxcnt)
throw new UiException(cnt > 0 ? "Too many concurrent push connections per session: " + cnt
: "Server push is disabled");
if (sp != null) {
_spush = sp;
} else {
final Class cls = getDevice().getServerPushClass();
if (cls == null)
throw new UiException(
"No server push defined. Make sure you are using ZK PE or EE, or you have configured your own implementation");
_spush = ((WebAppCtrl) _wapp).getUiFactory().newServerPush(this, cls);
}
_spush.start(this);
_spushShallStop = false;
++cnt;
} else if (_spush.isActive()) {
if (enablers.isEmpty()) {
_spushShallStop = true;
--cnt;
}
} else {
_spush.stop();
_spush = null;
--cnt;
}
_sess.setAttribute(ATTR_PUSH_COUNT, new Integer(cnt));
} else if (enable) {
//B65-ZK-1840 make sure the serverpush resumes in case stopped during that executions
_spushShallStop = false;
}
return serverPushAlreadyExists;
}
public boolean isServerPushEnabled() {
return _spush != null;
}
public ServerPush getServerPush() {
return _spush;
}
public <T extends Event> void scheduleServerPush(EventListener<T> listener, T event) {
if (listener == null)
throw new IllegalArgumentException("null listener");
checkSeverPush("schedule");
_spush.schedule(listener, event, new Scheduler<T>() {
public void schedule(EventListener<T> listener, T event) {
if (log.isDebugEnabled()) {
log.debug("scheduleServerPush: [{}]", event);
}
synchronized (_schedInfos) {
if (!scheduledServerPush()) {
// reset write/read count
_scheduleInfoReadCount.set(0);
_scheduleInfoWriteCount.set(0);
}
_schedInfos.put(_scheduleInfoWriteCount.getAndIncrement(),
new ScheduleInfo<T>(listener, event));
}
}
});
}
public boolean scheduledServerPush() {
return !_schedInfos.asMap().isEmpty(); //no need to sync
}
private void checkSeverPush(String what) {
if (_spush == null)
if (isAlive())
throw new IllegalStateException(
"Before calling Executions." + what + "(), the server push must be enabled for " + this);
else
throw new DesktopUnavailableException("Stopped");
}
public boolean activateServerPush(long timeout) throws InterruptedException {
checkSeverPush("activate");
if (Events.inEventListener() && Executions.getCurrent().getDesktop() == this)
throw new IllegalStateException("No need to invoke Executions.activate() in an event listener");
if (log.isDebugEnabled()) {
log.debug("activateServerPush, timeout is [{}]", timeout);
}
return _spush.activate(timeout);
}
public void deactivateServerPush() {
if (log.isDebugEnabled()) {
log.debug("activateServerPush, _spush's activation is [{}]", _spush.isActive());
}
if (_spush != null)
if (_spush.deactivate(_spushShallStop)) {
_spushShallStop = false;
_spush = null;
}
}
public void onPiggybackListened(Component comp, boolean listen) {
//we don't cache comp to avoid the risk of memory leak (maybe not
//a problem)
//On the other hand, most pages don't listen onPiggyback at all,
//so _piggybackListened is good enough to improve the performance
if (listen)
_piggybackListened = true;
}
public void onPiggyback() {
//Note: we don't post ON_PIGGYBACK twice in an execution
//(performance concern and back-compatibility).
if (_piggybackListened && Executions.getCurrent().getAttribute(ATTR_PIGGYBACK_POSTED) == null) {
for (Page page : _pages) {
if (Executions.getCurrent().isAsyncUpdate(page)) { //ignore new created pages
for (Component root = page.getFirstRoot(); root != null; root = root.getNextSibling()) {
if (Events.isListened(root, Events.ON_PIGGYBACK, false)) { //asap+deferrable
Events.postEvent(new Event(Events.ON_PIGGYBACK, root));
Executions.getCurrent().setAttribute(ATTR_PIGGYBACK_POSTED, Boolean.TRUE);
}
}
}
}
}
if (scheduledServerPush())
Events.postEvent(ON_SCHEDULE, _dummyTarget, null);
//we could not process them here (otherwise, event handling, thread
//might not work)
//Thus, we post an event and handle it in _dummyTarget
if (_spush != null)
_spush.onPiggyback();
}
private static final String ATTR_PIGGYBACK_POSTED = "org.zkoss.zk.ui.impl.piggyback.posted";
//AU Response//
public void responseSent(String reqId, Object response) {
if (reqId != null)
_lastRes = new ReqResult(reqId, response);
}
public Object getLastResponse(String reqId) {
return _lastRes != null && _lastRes.id.equals(reqId) ? _lastRes.response : null;
}
public int getResponseId(boolean advance) {
if (advance && ++_resId > MAX_RESPONSE_ID)
_resId = 1;
return _resId;
}
public void setResponseId(int resId) {
if (resId > MAX_RESPONSE_ID)
throw new IllegalArgumentException("Invalid response ID: " + resId);
_resId = resId < 0 ? 0 : resId;
}
public List<AuResponse> piggyResponse(Collection<AuResponse> response, boolean reset) {
if (response != null) {
if (_piggyRes == null)
_piggyRes = new LinkedList<AuResponse>();
_piggyRes.addAll(response);
}
List<AuResponse> l = _piggyRes;
if (reset)
_piggyRes = null;
return l;
}
public void invalidate() {
for (Page page : _pages)
if (((PageCtrl) page).getOwner() == null)
page.invalidate();
}
public Storage getStorage() {
_storageLock.lock();
try {
Storage storage = (Storage) getAttribute(this.getClass().getName());
if (storage == null) {
setAttribute(this.getClass().getName(), storage = new Storage() {
private ConcurrentHashMap<String, Object> _cache = new ConcurrentHashMap<String, Object>();
public <T> T getItem(String key) {
return (T) _cache.get(key);
}
public <T> void setItem(String key, T value) {
_cache.put(key, (Object) value);
}
public <T> T removeItem(String key) {
return (T) _cache.remove(key);
}
});
}
return storage;
} finally {
_storageLock.unlock();
}
}
public void pushHistoryState(Object state, String title, String url) {
addResponse(new AuHistoryState(false, state, title, url));
}
public void replaceHistoryState(Object state, String title, String url) {
addResponse(new AuHistoryState(true, state, title, url));
}
private static class ReqResult {
private final String id;
private final Object response;
private ReqResult(String id, Object response) {
this.id = id;
this.response = response;
}
}
private static class RecycleInfo implements java.io.Serializable {
private final int execId;
private final List<String> uuids = new LinkedList<String>();
private RecycleInfo(int execId) {
this.execId = execId;
}
public String toString() {
return '[' + execId + ": " + uuids + ']';
}
}
private static class ScheduleInfo<T extends Event> implements java.io.Serializable {
private final EventListener<T> _listener;
private final T _event;
private ScheduleInfo(EventListener<T> listener, T event) {
_listener = listener;
_event = event;
}
private void invoke() throws Exception {
if (log.isDebugEnabled()) {
log.debug("Handling schedule info, the event is [{}]", _event);
}
_listener.onEvent(_event);
}
}
private class ScheduleListener implements EventListener<Event>, java.io.Serializable {
public void onEvent(Event event) throws Exception {
final long max = System.currentTimeMillis() + getMaxSchedTime();
if (log.isDebugEnabled()) {
log.debug("Handling schedule server push, _schedInfos is empty: [{}]", !scheduledServerPush());
}
while (scheduledServerPush()) {
final Integer key = _scheduleInfoReadCount.getAndIncrement();
ScheduleInfo<? extends Event> ifPresent = _schedInfos.getIfPresent(
key);
if (ifPresent != null) {
ifPresent.invoke();
_schedInfos.invalidate(key);
} else if (scheduledServerPush()) {
synchronized (_schedInfos) {
// just in case to avoid endless loop
List<Integer> keys = new ArrayList<>(
_schedInfos.asMap().keySet());
if (!keys.isEmpty()) {
Collections.sort(keys);
_scheduleInfoReadCount.set(keys.get(0));
continue;
} else {
break;
}
}
}
if (System.currentTimeMillis() > max)
break; //avoid if server push is coming too fast
}
}
}
/** The maximal allowed time to run scheduled listeners.
*/
private static long getMaxSchedTime() {
if (_maxSchedTime == null) {
//no need to be synchronized
final String PROP = "org.zkoss.zk.ui.maxScheduleTime";
final String val = Library.getProperty(PROP);
if (val != null) {
try {
int v = Integer.parseInt(val); //unit: seconds
if (v > 0)
_maxSchedTime = ((long) v) * 1000;
} catch (Throwable t) {
log.warn("Ignored library property, " + PROP + ": not a number, " + val);
}
}
if (_maxSchedTime == null)
_maxSchedTime = 5000L; //default: 5 seconds
}
return _maxSchedTime.longValue();
}
private static Long _maxSchedTime;
}