/* 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;
}
