/* UiEngineImpl.java

	Purpose:

	Description:

	History:
		Thu Jun  9 13:05:28     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.IOException;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;

import javax.servlet.ServletRequest;

import org.owasp.encoder.Encode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.json.JSONArray;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Expectable;
import org.zkoss.lang.Library;
import org.zkoss.util.ArraysX;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.au.AuWriter;
import org.zkoss.zk.au.AuWriters;
import org.zkoss.zk.au.RequestOutOfSequenceException;
import org.zkoss.zk.au.out.AuAlert;
import org.zkoss.zk.au.out.AuSetAttribute;
import org.zkoss.zk.au.out.AuSetAttributes;
import org.zkoss.zk.au.out.AuWrongValue;
import org.zkoss.zk.mesg.MZk;
import org.zkoss.zk.ui.ActivationTimeoutException;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.Richlet;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.Sessions;
import org.zkoss.zk.ui.SuspendNotAllowedException;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.WrongValuesException;
import org.zkoss.zk.ui.event.CreateEvent;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.EventThreadCleanup;
import org.zkoss.zk.ui.event.EventThreadInit;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.FulfillEvent;
import org.zkoss.zk.ui.ext.AfterCompose;
import org.zkoss.zk.ui.ext.Native;
import org.zkoss.zk.ui.ext.Scope;
import org.zkoss.zk.ui.ext.Scopes;
import org.zkoss.zk.ui.ext.render.PrologAllowed;
import org.zkoss.zk.ui.metainfo.AttributesInfo;
import org.zkoss.zk.ui.metainfo.ComponentDefinition;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;
import org.zkoss.zk.ui.metainfo.NativeInfo;
import org.zkoss.zk.ui.metainfo.NodeInfo;
import org.zkoss.zk.ui.metainfo.PageDefinition;
import org.zkoss.zk.ui.metainfo.Property;
import org.zkoss.zk.ui.metainfo.ShadowInfo;
import org.zkoss.zk.ui.metainfo.TemplateInfo;
import org.zkoss.zk.ui.metainfo.TextInfo;
import org.zkoss.zk.ui.metainfo.VariablesInfo;
import org.zkoss.zk.ui.metainfo.ZScriptInfo;
import org.zkoss.zk.ui.metainfo.ZkInfo;
import org.zkoss.zk.ui.sys.AbortingReason;
import org.zkoss.zk.ui.sys.Attributes;
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.FailoverManager;
import org.zkoss.zk.ui.sys.PageConfig;
import org.zkoss.zk.ui.sys.PageCtrl;
import org.zkoss.zk.ui.sys.RequestQueue;
import org.zkoss.zk.ui.sys.SessionCtrl;
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.Callback;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zk.ui.util.ComposerExt;
import org.zkoss.zk.ui.util.Condition;
import org.zkoss.zk.ui.util.Configuration;
import org.zkoss.zk.ui.util.ExecutionMonitor;
import org.zkoss.zk.ui.util.ForEach;
import org.zkoss.zk.ui.util.FullComposer;
import org.zkoss.zk.ui.util.Monitor;
import org.zkoss.zk.ui.util.PerformanceMeter;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zk.ui.util.TemplateCtrl;
import org.zkoss.zk.xel.Evaluators;

/**
 * An implementation of {@link UiEngine} to create and update components.
 *
 * @author tomyeh
 */
public class UiEngineImpl implements UiEngine {
	/*package*/ static final Logger log = LoggerFactory.getLogger(UiEngineImpl.class);

	/** The Web application this engine belongs to. */
	private WebApp _wapp;
	/** A pool of idle EventProcessingThreadImpl. */
	private final List<EventProcessingThreadImpl> _idles = new LinkedList<EventProcessingThreadImpl>();
	/** A map of suspended processing:
	 * (Desktop desktop, IdentityHashMap(Object mutex, List(EventProcessingThreadImpl)).
	 */
	private final Map<Desktop, Map<Object, List<EventProcessingThreadImpl>>> _suspended = new HashMap<Desktop, Map<Object, List<EventProcessingThreadImpl>>>();
	/** A map of resumed processing
	 * (Desktop desktop, List(EventProcessingThreadImpl)).
	 */
	private final Map<Desktop, List<EventProcessingThreadImpl>> _resumed = new HashMap<Desktop, List<EventProcessingThreadImpl>>();
	/** # of suspended event processing threads.
	 */
	private int _suspCnt;

	public UiEngineImpl() {
	}

	//-- UiEngine --//
	public void start(WebApp wapp) {
		_wapp = wapp;
	}

	public void stop(WebApp wapp) {
		synchronized (_idles) {
			for (EventProcessingThreadImpl thread : _idles)
				thread.cease("Stop application");
			_idles.clear();
		}

		synchronized (_suspended) {
			for (Map<Object, List<EventProcessingThreadImpl>> map : _suspended.values()) {
				synchronized (map) {
					for (List<EventProcessingThreadImpl> threads : map.values()) {
						for (EventProcessingThreadImpl thread : threads)
							thread.cease("Stop application");
					}
				}
			}
			_suspended.clear();
		}
		synchronized (_resumed) {
			for (List<EventProcessingThreadImpl> threads : _resumed.values()) {
				synchronized (threads) {
					for (EventProcessingThreadImpl thread : threads)
						thread.cease("Stop application");
				}
			}
			_resumed.clear();
		}
	}

	public boolean hasSuspendedThread() {
		if (!_suspended.isEmpty()) {
			synchronized (_suspended) {
				for (Map map : _suspended.values())
					if (!map.isEmpty())
						return true;
			}
		}
		return false;
	}

	public Collection<EventProcessingThread> getSuspendedThreads(Desktop desktop) {
		final Map<Object, List<EventProcessingThreadImpl>> map;
		synchronized (_suspended) {
			map = _suspended.get(desktop);
		}

		if (map == null || map.isEmpty())
			return Collections.emptyList();

		final List<EventProcessingThread> threads = new LinkedList<EventProcessingThread>();
		synchronized (map) {
			for (List<EventProcessingThreadImpl> thds : map.values()) {
				threads.addAll(thds);
			}
		}
		return threads;
	}

	public boolean ceaseSuspendedThread(Desktop desktop, EventProcessingThread evtthd, String cause) {
		final Map<Object, List<EventProcessingThreadImpl>> map;
		synchronized (_suspended) {
			map = _suspended.get(desktop);
		}
		if (map == null)
			return false;

		boolean found = false;
		synchronized (map) {
			for (Iterator<Map.Entry<Object, List<EventProcessingThreadImpl>>> it = map.entrySet().iterator(); it
					.hasNext();) {
				final Map.Entry<Object, List<EventProcessingThreadImpl>> me = it.next();
				final List<EventProcessingThreadImpl> list = me.getValue();
				found = list.remove(evtthd); //found
				if (found) {
					if (list.isEmpty())
						it.remove(); //(mutex, list) no longer useful
					break; //DONE
				}
			}
		}
		if (found)
			((EventProcessingThreadImpl) evtthd).cease(cause);
		return found;
	}

	public void desktopDestroyed(Desktop desktop) {
		//		if (log.isDebugEnabled()) log.debug("destroy "+desktop);

		Execution exec = Executions.getCurrent();
		if (exec == null) {
			//Bug 2015878: exec is null if it is caused by session invalidated
			//while listener (ResumeAbort and so) might need it
			exec = new PhantomExecution(desktop);
			boolean activated = activate(exec, getDestroyTimeout());
			try {
				desktopDestroyed0(desktop);
			} finally {
				if (activated)
					deactivate(exec);
			}
		} else {
			desktopDestroyed0(desktop);
		}
	}

	private void desktopDestroyed0(Desktop desktop) {
		final Configuration config = _wapp.getConfiguration();
		if (!_suspended.isEmpty()) { //no need to sync (better performance)
			final Map<Object, List<EventProcessingThreadImpl>> map;
			synchronized (_suspended) {
				map = _suspended.remove(desktop);
			}
			if (map != null) {
				synchronized (map) {
					for (List<EventProcessingThreadImpl> list : map.values()) {
						for (EventProcessingThreadImpl evtthd : list) {
							evtthd.ceaseSilently("Destroy desktop " + desktop);
							config.invokeEventThreadResumeAborts(evtthd.getComponent(), evtthd.getEvent());
						}
					}
				}
			}
		}

		if (!_resumed.isEmpty()) { //no need to sync (better performance)
			final List<EventProcessingThreadImpl> list;
			synchronized (_resumed) {
				list = _resumed.remove(desktop);
			}
			if (list != null) {
				synchronized (list) {
					for (EventProcessingThreadImpl evtthd : list) {
						evtthd.ceaseSilently("Destroy desktop " + desktop);
						config.invokeEventThreadResumeAborts(evtthd.getComponent(), evtthd.getEvent());
					}
				}
			}
		}

		((DesktopCtrl) desktop).destroy();
	}

	private static UiVisualizer getCurrentVisualizer() {
		final ExecutionCtrl execCtrl = ExecutionsCtrl.getCurrentCtrl();
		if (execCtrl == null)
			throw new IllegalStateException("Components can be accessed only in event listeners");
		return (UiVisualizer) execCtrl.getVisualizer();
	}

	public Component setOwner(Component comp) {
		return getCurrentVisualizer().setOwner(comp);
	}

	public boolean isInvalidated(Component comp) {
		return getCurrentVisualizer().isInvalidated(comp);
	}

	public void addInvalidate(Page page) {
		if (page == null)
			throw new IllegalArgumentException();
		getCurrentVisualizer().addInvalidate(page);
	}

	public void addInvalidate(Component comp) {
		if (comp == null)
			throw new IllegalArgumentException();
		getCurrentVisualizer().addInvalidate(comp);
	}

	public void addSmartUpdate(Component comp, String attr, Object value, boolean append) {
		getCurrentVisualizer().addSmartUpdate(comp, attr, value, append);
	}

	public void addSmartUpdate(Component comp, String attr, Object value, int priority) {
		getCurrentVisualizer().addSmartUpdate(comp, attr, value, priority);
	}

	public void clearSmartUpdate(Component comp) {
		getCurrentVisualizer().clearSmartUpdate(comp);
	}

	public void addResponse(AuResponse response) {
		getCurrentVisualizer().addResponse(response);
	}

	public void addResponse(String key, AuResponse response) {
		getCurrentVisualizer().addResponse(key, response);
	}

	public void addResponse(String key, AuResponse response, int priority) {
		getCurrentVisualizer().addResponse(key, response, priority);
	}

	public void addMoved(Component comp, Component oldparent, Page oldpg, Page newpg) {
		if (comp == null)
			throw new IllegalArgumentException();
		getCurrentVisualizer().addMoved(comp, oldparent, oldpg, newpg);
	}

	public void addUuidChanged(Component comp) {
		if (comp == null)
			throw new IllegalArgumentException();
		getCurrentVisualizer().addUuidChanged(comp);
	}

	public boolean disableClientUpdate(Component comp, boolean disable) {
		return getCurrentVisualizer().disableClientUpdate(comp, disable);
	}

	//-- Creating a new page --//
	public void execNewPage(Execution exec, Richlet richlet, Page page, Writer out) throws IOException {
		execNewPage0(exec, null, richlet, page, out);
	}

	public void execNewPage(Execution exec, PageDefinition pagedef, Page page, Writer out) throws IOException {
		execNewPage0(exec, pagedef, null, page, out);
	}

	/** It assumes exactly one of pagedef and richlet is not null. */
	private void execNewPage0(final Execution exec, final PageDefinition pagedef, final Richlet richlet,
			final Page page, final Writer out) throws IOException {
		//Update the device type first. If this is the second page and not
		//belonging to the same device type, an exception is thrown
		final Desktop desktop = exec.getDesktop();
		final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
		final LanguageDefinition langdef = //default page
		pagedef != null ? pagedef.getLanguageDefinition() : richlet != null ? richlet.getLanguageDefinition() : null;
		if (langdef != null)
			desktop.setDeviceType(langdef.getDeviceType()); //set and check!

		final WebApp wapp = desktop.getWebApp();
		final Configuration config = wapp.getConfiguration();
		PerformanceMeter pfmeter = config.getPerformanceMeter();
		final long startTime = pfmeter != null ? System.currentTimeMillis() : 0;
		//snapshot time since activate might take time

		//It is possible this method is invoked when processing other exec
		final Execution oldexec = Executions.getCurrent();
		final ExecutionCtrl oldexecCtrl = (ExecutionCtrl) oldexec;
		final UiVisualizer olduv = oldexecCtrl != null ? (UiVisualizer) oldexecCtrl.getVisualizer() : null;

		final UiVisualizer uv;
		if (olduv != null) {
			uv = doReactivate(exec, olduv);
			pfmeter = null; //don't count included pages
		} else {
			uv = doActivate(exec, false, false, null, -1);
		}

		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		final Page old = execCtrl.getCurrentPage();
		final PageDefinition olddef = execCtrl.getCurrentPageDefinition();
		execCtrl.setCurrentPage(page);
		execCtrl.setCurrentPageDefinition(pagedef);

		final String pfReqId = pfmeter != null ? meterLoadStart(pfmeter, exec, startTime) : null;
		AbortingReason abrn = null;
		try {
			config.invokeExecutionInits(exec, oldexec);
			desktopCtrl.invokeExecutionInits(exec, oldexec);

			if (olduv != null) {
				final Component owner = olduv.getOwner();
				if (owner != null) {
					((PageCtrl) page).setOwner(owner);
					//					if (log.finerable()) log.finer("Set owner of "+page+" to "+owner);
				}
			}

			//Cycle 1: Creates all components

			//Note:
			//1) stylesheet, tablib are inited in Page's contructor
			//2) we add variable resolvers before init because
			//init's zscirpt might depend on it.
			if (pagedef != null) {
				((PageCtrl) page).preInit();
				pagedef.preInit(page);

				final Initiators inits = Initiators.doInit(pagedef, page, config.getInitiators());
				//F1472813: sendRedirect in init; test: redirectNow.zul
				try {
					pagedef.init(page, !uv.isEverAsyncUpdate() && !uv.isAborting());

					//ZK-2623: page scope template
					Map<String, TemplateInfo> pageTemplates = pagedef.getTemplatesInfo();
					if (pageTemplates != null) {
						for (Map.Entry<String, TemplateInfo> entry : pageTemplates.entrySet()) {
							page.addTemplate(entry.getKey(), new TemplateImpl(entry.getValue(), null));
						}
					}

					final Component[] comps;
					final String uri = pagedef.getForwardURI(page);
					if (uri != null) {
						comps = new Component[0];
						try {
							exec.forward(uri);
						} finally { //ZK-1584: should cleanup after forward
							final List<Throwable> errs = new LinkedList<Throwable>();

							desktopCtrl.invokeExecutionCleanups(exec, oldexec, errs);
							config.invokeExecutionCleanups(exec, oldexec, errs);
						}
					} else {
						exec.setAttribute(org.zkoss.zk.ui.impl.Attributes.PAGE_CREATED, Boolean.TRUE);
						comps = uv.isAborting() || exec.isVoided() ? new Component[0]
								: execCreate(new CreateInfo(((WebAppCtrl) wapp).getUiFactory(), exec, page,
										config.getComposer(page)), pagedef, null, null);
					}

					inits.doAfterCompose(page, comps);
					afterCreate(exec, comps);
				} catch (Throwable ex) {
					if (!inits.doCatch(ex))
						throw UiException.Aide.wrap(ex);
				} finally {
					inits.doFinally();
				}
			} else {
				//FUTURE: a way to allow richlet to set page ID
				((PageCtrl) page).preInit();

				final Initiators inits = Initiators.doInit(null, page, config.getInitiators());
				try {
					((PageCtrl) page).init(new PageConfig() {
						public String getId() {
							return null;
						}

						public String getUuid() {
							return null;
						}

						public String getTitle() {
							return null;
						}

						public String getStyle() {
							return null;
						}

						public String getViewport() {
							return "auto";
						}

						public String getBeforeHeadTags() {
							return "";
						}

						public String getAfterHeadTags() {
							return "";
						}

						public Collection<Object[]> getResponseHeaders() {
							return Collections.emptyList();
						}
					});
					final Composer composer = config.getComposer(page);
					try {
						richlet.service(page);

						for (Component root = page.getFirstRoot(); root != null; root = root.getNextSibling()) {
							doAfterCompose(composer, root);
							afterCreate(exec, new Component[] { root });
							//root's next sibling might be changed
						}
					} catch (Throwable t) {
						if (composer instanceof ComposerExt)
							if (((ComposerExt) composer).doCatch(t))
								t = null; //ignored
						if (t != null)
							throw t;
					} finally {
						if (composer instanceof ComposerExt)
							((ComposerExt) composer).doFinally();
					}
				} catch (Throwable ex) {
					if (!inits.doCatch(ex))
						throw UiException.Aide.wrap(ex);
				} finally {
					inits.doFinally();
				}
			}
			if (exec.isVoided())
				return; //don't generate any output

			//Cycle 2: process pending events
			//Unlike execUpdate, execution is aborted here if any exception
			final List<Throwable> errs = new LinkedList<Throwable>();
			Event event = nextEvent(uv);
			do {
				for (; event != null; event = nextEvent(uv)) {
					try {
						process(desktop, event);
					} catch (Throwable ex) {
						handleError(ex, uv, errs);
					}
				}

				resumeAll(desktop, uv, null);
			} while ((event = nextEvent(uv)) != null);

			//Cycle 2a: Handle aborting reason
			abrn = uv.getAbortingReason();
			if (abrn != null)
				abrn.execute(); //always execute even if !isAborting

			//Cycle 3: Redraw the page (and responses)
			List<AuResponse> responses = getResponses(exec, uv, errs, false);

			if (olduv != null && olduv.addToFirstAsyncUpdate(responses))
				responses = null;
			//A new ZK page might be included by an async update
			//(example: ZUL's include).
			//If so, we cannot generate the responses in the page.
			//Rather, we shall add them to the async update.
			else
				execCtrl.setResponses(responses);

			((PageCtrl) page).redraw(out);
			afterRenderNewPage(page);

			desktopCtrl.invokeExecutionCleanups(exec, oldexec, errs);
			config.invokeExecutionCleanups(exec, oldexec, errs);
		} catch (Throwable ex) {
			final List<Throwable> errs = new LinkedList<Throwable>();
			errs.add(ex);

			desktopCtrl.invokeExecutionCleanups(exec, oldexec, errs);
			config.invokeExecutionCleanups(exec, oldexec, errs);

			if (!errs.isEmpty()) {
				ex = errs.get(0);
				if (ex instanceof IOException)
					throw (IOException) ex;
				throw UiException.Aide.wrap(ex);
			}
		} finally {
			if (abrn != null) {
				try {
					abrn.finish();
				} catch (Throwable t) {
					log.warn("", t);
				}
			}

			execCtrl.setCurrentPage(old); //restore it
			execCtrl.setCurrentPageDefinition(olddef); //restore it

			if (olduv != null)
				doDereactivate(exec, olduv);
			else
				doDeactivate(exec);

			if (pfmeter != null)
				meterLoadServerComplete(pfmeter, pfReqId, exec);
		}
	}

	@SuppressWarnings("unchecked")
	private static final void doAfterCompose(Composer composer, Component comp) throws Exception {
		if (composer != null)
			composer.doAfterCompose(comp);
	}

	public void recycleDesktop(Execution exec, Page page, Writer out) throws IOException {
		PerformanceMeter pfmeter = page.getDesktop().getWebApp().getConfiguration().getPerformanceMeter();
		final long startTime = pfmeter != null ? System.currentTimeMillis() : 0;
		final String pfReqId = pfmeter != null ? meterLoadStart(pfmeter, exec, startTime) : null;

		final UiVisualizer uv = doActivate(exec, false, false, null, -1);
		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		execCtrl.setCurrentPage(page);

		try {
			Events.postEvent(new Event(Events.ON_DESKTOP_RECYCLE));

			final List<Throwable> errs = new LinkedList<Throwable>();
			final Desktop desktop = exec.getDesktop();

			//ZK-1777 resume existing serverpush on a recycled desktop
			if (desktop.isServerPushEnabled()) {
				((DesktopCtrl) desktop).getServerPush().resume();
			}

			Event event = nextEvent(uv);
			do {
				for (; event != null; event = nextEvent(uv)) {
					try {
						process(desktop, event);
					} catch (Throwable ex) {
						handleError(ex, uv, errs);
					}
				}
				resumeAll(desktop, uv, null);
			} while ((event = nextEvent(uv)) != null);

			execCtrl.setResponses(getResponses(exec, uv, errs, false));

			((PageCtrl) page).redraw(out);
		} finally {
			doDeactivate(exec);
			if (pfmeter != null)
				meterLoadServerComplete(pfmeter, pfReqId, exec);
		}
	}

	/** Called after the whole component tree has been created by
	 * this engine.
	 * @param comps the components being created. It is never null but
	 * it might be a zero-length array.
	 */
	private void afterCreate(Execution exec, Component[] comps) {
		afterCreate(exec, getExtension(), comps);
	}

	private static void afterCreate(Execution exec, Extension ext, Component[] comps) {
		if (ext == null)
			ext = ((UiEngineImpl) ((WebAppCtrl) exec.getDesktop().getWebApp()).getUiEngine()).getExtension();
		ext.afterCreate(comps);

	}

	/** Called after a new page has been redrawn ({@link PageCtrl#redraw}
	 * has been called).
	 */
	private void afterRenderNewPage(Page page) {
		getExtension().afterRenderNewPage(page);
	}

	/** Called when this engine renders the given components.
	 * @param comps the collection of components that have been redrawn.
	 */
	protected void afterRenderComponents(Collection<Component> comps) {
		getExtension().afterRenderComponents(comps);
	}
	private static class ExtensionHolder {
		private static final Extension INSTANCE = initializeExtension();

		private static Extension initializeExtension() {
			String clsnm = Library.getProperty("org.zkoss.zk.ui.impl.UiEngineImpl.extension");
			if (clsnm != null) {
				try {
					return (Extension) Classes.newInstanceByThread(clsnm);
				} catch (Throwable ex) {
					log.error("Unable to instantiate " + clsnm, ex);
				}
			}
			return new DefaultExtension();
		}
	}

	private Extension getExtension() {
		return ExtensionHolder.INSTANCE;
	}

	private static final Event nextEvent(UiVisualizer uv) {
		final Event evt = ((ExecutionCtrl) uv.getExecution()).getNextEvent();
		if (evt != null && !uv.isAborting() && (Sessions.getCurrent() == null
				|| !((SessionCtrl) Sessions.getCurrent()).isInvalidated())) {
			return evt;
		}
		return null;
	}

	/** Cycle 1:
	 * Creates all child components defined in the specified definition.
	 * @return the first component being created.
	 */
	private static final Component[] execCreate(CreateInfo ci, NodeInfo parentInfo, Component parent,
			Component insertBefore) {
		String fulfillURI = null;
		if (parentInfo instanceof ComponentInfo) {
			final ComponentInfo pi = (ComponentInfo) parentInfo;
			String fulfill = pi.getFulfill();
			if (fulfill != null) { //defer the creation of children
				fulfill = fulfill.trim();
				if (fulfill.length() > 0) {
					if (fulfill.charAt(0) == '=') {
						fulfillURI = fulfill.substring(1).trim();
					} else {
						new FulfillListener(fulfill, pi, parent);
						return new Component[0];
					}
				}
			}
		}

		Component[] cs = execCreate0(ci, parentInfo, parent, insertBefore);

		if (fulfillURI != null) {
			fulfillURI = (String) Evaluators.evaluate(((ComponentInfo) parentInfo).getEvaluator(), parent, fulfillURI,
					String.class);
			if (fulfillURI != null) {
				cs = merge(cs, ci.exec.createComponents(fulfillURI, parent, insertBefore, null));
			}
		}

		return cs;
	}

	private static Component[] merge(Component[] cs, Component c) {
		if (c != null) {
			cs = ArraysX.resize(cs, cs.length + 1);
			cs[cs.length - 1] = c;
		}
		return cs;
	}

	private static final Component[] execCreate0(CreateInfo ci, NodeInfo parentInfo, Component parent,
			Component insertBefore) {
		final List<Component> created = new LinkedList<Component>();
		final Page page = ci.page;
		final PageDefinition pagedef = parentInfo.getPageDefinition();
		//note: don't use page.getDefinition because createComponents
		//might be called from a page other than instance's
		if (!parentInfo.getChildren().isEmpty()) {
			final ReplaceableText replaceableText = new ReplaceableText();
			for (final NodeInfo meta : parentInfo.getChildren()) {
				if (meta instanceof ComponentInfo) {
					final ComponentInfo childInfo = (ComponentInfo) meta;
					final ForEach forEach = childInfo.resolveForEach(page, parent);
					if (forEach == null) {
						if (isEffective(childInfo, page, parent)) {
							final Component[] children = execCreateChild(ci, parent, childInfo, replaceableText,
									insertBefore);
							Collections.addAll(created, children);
						}
					} else {
						while (forEach.next()) {
							if (isEffective(childInfo, page, parent)) {
								final Component[] children = execCreateChild(ci, parent, childInfo, replaceableText,
										insertBefore);
								Collections.addAll(created, children);
							}
						}
					}
				} else if (meta instanceof ZkInfo) {
					final ZkInfo childInfo = (ZkInfo) meta;
					final ForEach forEach = childInfo.resolveForEach(page, parent);
					if (forEach == null) {
						if (isEffective(childInfo, page, parent)) {
							final Component[] children = execCreateChild(ci, parent, childInfo, replaceableText,
									insertBefore);
							Collections.addAll(created, children);
						}
					} else {
						while (forEach.next()) {
							if (isEffective(childInfo, page, parent)) {
								final Component[] children = execCreateChild(ci, parent, childInfo, replaceableText,
										insertBefore);
								Collections.addAll(created, children);
							}
						}
					}
				} else if (meta instanceof TextInfo) {
					//parent must be a native component
					final String s = ((TextInfo) meta).getValue(parent);
					if (s != null && s.length() > 0)
						if (parent != null) {
							parent.insertBefore(((Native) parent).getHelper().newNative(s), insertBefore);
						} else if (page != null) {
							created.add(ci.uf.newComponent(page, null,
									page.getLanguageDefinition().newLabelInfo(null, s), insertBefore));
						} else {
							throw new UnsupportedOperationException("parent or page required for native label: " + s);
						}
				} else if (meta instanceof ShadowInfo) {
					final ShadowInfo shadow = (ShadowInfo) meta;
					if (isEffective(shadow, page, parent)) {
						final Component[] children = execCreateChild(ci, parent, shadow, insertBefore);
						Collections.addAll(created, children);
					}
				} else {
					execNonComponent(ci, parent, meta);
				}
			}
		}
		return created.toArray(new Component[created.size()]);
	}

	private static Component[] execCreateChild(CreateInfo ci, Component parent, ZkInfo childInfo,
			ReplaceableText replaceableText, Component insertBefore) {
		return childInfo.withSwitch() ? execSwitch(ci, childInfo, parent, insertBefore)
				: execCreate0(ci, childInfo, parent, insertBefore);
	}

	private static Component[] execCreateChild(CreateInfo ci, Component parent, ShadowInfo childInfo,
			Component insertBefore) {
		Component child = null;
		final boolean bRoot = parent == null;
		try {
			// None composer support for shadow element

			child = ci.uf.newComponent(ci.page, parent, childInfo, insertBefore);

			childInfo.apply(child); // apply the property from ShadowInfo

			execCreate(ci, childInfo, child, null); //recursive (and appendChild)

			if (child instanceof AfterCompose)
				((AfterCompose) child).afterCompose();
		} catch (Throwable ex) {
			boolean ignore = ci.doCatch(ex, bRoot);
			if (!ignore)
				throw UiException.Aide.wrap(ex);
		}

		return child != null ? new Component[] { child } : new Component[0];
	}

	private static Component[] execCreateChild(CreateInfo ci, Component parent, ComponentInfo childInfo,
			ReplaceableText replaceableText, Component insertBefore) {
		final ComponentDefinition childdef = childInfo.getComponentDefinition();
		if (childdef.isInlineMacro()) {
			if (insertBefore != null)
				throw new UnsupportedOperationException("The inline macro doesn't support template");

			final Map<String, Object> props = new HashMap<String, Object>();
			props.put("includer", parent);
			childInfo.evalProperties(props, ci.page, parent, true);
			return new Component[] { ci.exec.createComponents(childdef.getMacroURI(), parent, props) };
		} else {
			String rt = null;
			if (replaceableText != null) {
				// ZK-3549 should ignore blank but was not able to do so in Parser, e.g. applying template
				if (parent == null || parent.getDefinition().isBlankPreserved()) {
					rt = replaceableText.text;
				}
				replaceableText.text = childInfo.getReplaceableText();
				if (replaceableText.text != null)
					return new Component[0];
				//Note: replaceableText is one-shot only
				//So, replaceable text might not be generated
				//and it is ok since it is only blank string
			}

			Component child = execCreateChild0(ci, parent, childInfo, rt, insertBefore);
			return child != null ? new Component[] { child } : new Component[0];
		}
	}

	private static Component execCreateChild0(CreateInfo ci, Component parent, ComponentInfo childInfo,
			String replaceableText, Component insertBefore) {
		Composer composer = childInfo.resolveComposer(ci.page, parent);
		ComposerExt composerExt = null;
		boolean bPopComposer = false;
		if (composer instanceof FullComposer) {
			ci.pushFullComposer(composer);
			bPopComposer = true;
			composer = null; //ci will handle it
		} else if (composer instanceof ComposerExt) {
			composerExt = (ComposerExt) composer;
		}

		Component child = null;
		final boolean bRoot = parent == null;
		try {
			if (composerExt != null) {
				childInfo = composerExt.doBeforeCompose(ci.page, parent, childInfo);
				if (childInfo == null)
					return null;
			}
			childInfo = ci.doBeforeCompose(ci.page, parent, childInfo, bRoot);
			if (childInfo == null)
				return null;

			child = ci.uf.newComponent(ci.page, parent, childInfo, insertBefore);

			if (replaceableText != null) {
				final Object xc = ((ComponentCtrl) child).getExtraCtrl();
				if (xc instanceof PrologAllowed)
					((PrologAllowed) xc).setPrologContent(replaceableText);
			}

			final boolean bNative = childInfo instanceof NativeInfo;
			if (bNative)
				setProlog(ci, child, (NativeInfo) childInfo);

			doBeforeComposeChildren(composerExt, child);
			ci.doBeforeComposeChildren(child, bRoot);

			execCreate(ci, childInfo, child, null); //recursive (and appendChild)

			if (bNative)
				setEpilog(ci, child, (NativeInfo) childInfo);

			if (child instanceof AfterCompose)
				((AfterCompose) child).afterCompose();

			doAfterCompose(composer, child);
			ci.doAfterCompose(child, bRoot);

			ComponentsCtrl.applyForward(child, childInfo.getForward());
			//applies the forward condition
			//1) we did it after all child created, so it may reference
			//to it child (thought rarely happens)
			//2) we did it after afterCompose, so what specified
			//here has higher priority than class defined by application developers

			//Bug ZK-504: even might be listened later (in parent's composer)
			//See also ZK-759
			Events.postEvent(new CreateEvent(Events.ON_CREATE, child, ci.exec.getArg()));

			return child;
		} catch (Throwable ex) {
			boolean ignore = false;
			if (composerExt != null) {
				try {
					ignore = composerExt.doCatch(ex);
				} catch (Throwable t) {
					log.error("Failed to invoke doCatch for " + childInfo, t);
				}
			}
			if (!ignore) {
				ignore = ci.doCatch(ex, bRoot);
				if (!ignore)
					throw UiException.Aide.wrap(ex);
			}

			return child != null && child.getPage() != null ? child : null;
			//return child only if attached successfully
		} finally {
			try {
				if (composerExt != null)
					composerExt.doFinally();
				ci.doFinally(bRoot);
			} catch (Throwable ex) {
				throw UiException.Aide.wrap(ex);
			} finally {
				if (bPopComposer)
					ci.popFullComposer();
			}
		}
	}

	@SuppressWarnings("unchecked")
	/*package*/ static void doBeforeComposeChildren(ComposerExt composerExt, Component comp) throws Exception {
		if (composerExt != null)
			composerExt.doBeforeComposeChildren(comp);
	}

	/** Handles <zk switch>. */
	private static Component[] execSwitch(CreateInfo ci, ZkInfo switchInfo, Component parent, Component insertBefore) {
		final Page page = ci.page;
		if (!switchInfo.getChildren().isEmpty()) {
			final Object switchCond = switchInfo.resolveSwitch(page, parent);

			for (NodeInfo nodeInfo : switchInfo.getChildren()) {
				final ZkInfo caseInfo = (ZkInfo) nodeInfo;
				final ForEach forEach = caseInfo.resolveForEach(page, parent);
				if (forEach == null) {
					if (isEffective(caseInfo, page, parent) && isCaseMatched(caseInfo, page, parent, switchCond)) {
						return execCreateChild(ci, parent, caseInfo, null, insertBefore);
					}
				} else {
					final List<Component> created = new LinkedList<Component>();
					while (forEach.next()) {
						if (isEffective(caseInfo, page, parent) && isCaseMatched(caseInfo, page, parent, switchCond)) {
							final Component[] children = execCreateChild(ci, parent, caseInfo, null, insertBefore);
							Collections.addAll(created, children);
							return created.toArray(new Component[created.size()]);
							//only once (AND condition)
						}
					}
				}
			}
		}
		return new Component[0];
	}

	private static boolean isCaseMatched(ZkInfo caseInfo, Page page, Component parent, Object switchCond) {
		if (!caseInfo.withCase())
			return true; //default clause

		final Object[] caseValues = caseInfo.resolveCase(page, parent);
		for (Object caseValue : caseValues) {
			if (caseValue instanceof String && switchCond instanceof String) {
				final String casev = (String) caseValue;
				final int len = casev.length();
				if (len >= 2 && casev.charAt(0) == '/' && casev.charAt(len - 1) == '/') { //regex
					if (Pattern.compile(casev.substring(1, len - 1)).matcher((String) switchCond).matches())
						return true;
					else
						continue;
				}
			}
			if (Objects.equals(switchCond, caseValue))
				return true; //OR condition
		}
		return false;
	}

	/** Executes a non-component object, such as ZScript, AttributesInfo...
	 */
	private static final void execNonComponent(CreateInfo ci, Component comp, Object meta) {
		final Page page = ci.page;
		if (meta instanceof AttributesInfo) {
			final AttributesInfo attrs = (AttributesInfo) meta;
			if (comp != null)
				attrs.apply(comp); //it handles isEffective
			else
				attrs.apply(page);
		} else if (meta instanceof TemplateInfo) {
			final TemplateInfo tempInfo = (TemplateInfo) meta;
			if (isEffective(tempInfo, page, comp)) {
				if (comp == null)
					page.addTemplate(tempInfo.getName(), new TemplateImpl(tempInfo, comp));
				else
					comp.setTemplate(tempInfo.getName(), new TemplateImpl(tempInfo, comp));
			}
		} else if (meta instanceof ZScriptInfo) {
			//Spec fix since 6.0.0: if/unless shall be evaluated first
			final ZScriptInfo zsInfo = (ZScriptInfo) meta;
			if (isEffective(zsInfo, page, comp)) {
				if (zsInfo.isDeferred()) {
					((PageCtrl) page).addDeferredZScript(comp, zsInfo.getZScript());
					//isEffective is handled later
				} else {
					final Scope scope = Scopes.beforeInterpret(comp != null ? (Scope) comp : page);
					try {
						page.interpret(zsInfo.getLanguage(), zsInfo.getContent(page, comp), scope);
					} finally {
						Scopes.afterInterpret();
					}
				}
			}
		} else if (meta instanceof VariablesInfo) {
			final VariablesInfo vars = (VariablesInfo) meta;
			if (comp != null)
				vars.apply(comp); //it handles isEffective
			else
				vars.apply(page);
		} else {
			//Note: we don't handle ComponentInfo here, because
			//getNativeContent assumes no child component
			throw new IllegalStateException(meta + " not allowed in " + comp);
		}
	}

	private static final boolean isEffective(Condition cond, Page page, Component comp) {
		return comp != null ? cond.isEffective(comp) : cond.isEffective(page);
	}

	public Component[] createComponents(Execution exec, PageDefinition pagedef, Page page, Component parent,
			Component insertBefore, VariableResolver resolver, Map<?, ?> arg) {
		if (pagedef == null)
			throw new IllegalArgumentException("pagedef");

		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		if (parent != null) {
			//assign page only if parent is not null (rather, we create a fakepg later)
			final Page ppg = parent.getPage();
			if (ppg != null)
				page = ppg;
			else if (page == null)
				page = execCtrl.getCurrentPage();
		}

		if (!execCtrl.isActivated())
			throw new IllegalStateException("Not activated yet");

		final boolean fakepg = page == null;
		if (fakepg)
			page = new VolatilePage(pagedef); //fake

		final Desktop desktop = exec.getDesktop();
		final WebApp wapp = desktop.getWebApp();
		final Page prevpg = execCtrl.getCurrentPage();
		if (page != null && page != prevpg)
			execCtrl.setCurrentPage(page);
		final PageDefinition olddef = execCtrl.getCurrentPageDefinition();
		execCtrl.setCurrentPageDefinition(pagedef);
		exec.pushArg(arg != null ? arg : Collections.EMPTY_MAP);

		//Note: we add taglib, stylesheets and var-resolvers to the page
		//it might cause name pollution but we got no choice since they
		//are used as long as components created by this method are alive
		if (fakepg)
			((PageCtrl) page).preInit();
		pagedef.preInit(page);

		//Note: the forward directives are ignore in this case

		final Initiators inits = Initiators.doInit(pagedef, page, wapp.getConfiguration().getInitiators());
		if (resolver != null)
			exec.addVariableResolver(resolver);
		try {
			if (fakepg)
				pagedef.init(page, false);

			final Component[] comps = execCreate(new CreateInfo(((WebAppCtrl) wapp).getUiFactory(), exec, page, null), //technically sys composer can be used but we don't (to make it simple)
					pagedef, parent, insertBefore);
			inits.doAfterCompose(page, comps);

			//Notice: if parent is not null, comps[j].page == parent.page
			if (fakepg && parent == null)
				for (Component comp : comps)
					comp.detach();

			afterCreate(exec, comps);
			return comps;
		} catch (Throwable ex) {
			inits.doCatch(ex);
			throw UiException.Aide.wrap(ex);
		} finally {
			if (resolver != null)
				exec.removeVariableResolver(resolver);
			exec.popArg();
			execCtrl.setCurrentPage(prevpg); //restore it
			execCtrl.setCurrentPageDefinition(olddef); //restore it

			inits.doFinally();

			if (fakepg) {
				try {
					((DesktopCtrl) desktop).removePage(page);
				} catch (Throwable ex) {
					log.warn("", ex);
				}
				((PageCtrl) page).destroy();
			}
		}
	}

	public void sendRedirect(String uri, String target) {
		if (uri != null && uri.length() == 0)
			uri = null;
		final UiVisualizer uv = getCurrentVisualizer();
		uv.setAbortingReason(new AbortBySendRedirect(uri != null ? uv.getExecution().encodeURL(uri) : "", target));
	}

	public void setAbortingReason(AbortingReason aborting) {
		final UiVisualizer uv = getCurrentVisualizer();
		uv.setAbortingReason(aborting);
	}

	//-- Recovering desktop --//
	public void execRecover(Execution exec, FailoverManager failover) {
		final Desktop desktop = exec.getDesktop();
		final Session sess = desktop.getSession();

		doActivate(exec, false, true, null, -1); //it must not return null
		try {
			failover.recover(sess, exec, desktop);
		} finally {
			doDeactivate(exec);
		}
	}

	//-- Asynchronous updates --//
	public void beginUpdate(Execution exec) {
		final UiVisualizer uv = doActivate(exec, true, false, null, -1);
		final Desktop desktop = exec.getDesktop();
		desktop.getWebApp().getConfiguration().invokeExecutionInits(exec, null);
		((DesktopCtrl) desktop).invokeExecutionInits(exec, null);
	}

	public void endUpdate(Execution exec) throws IOException {
		final Desktop desktop = exec.getDesktop();
		final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
		final Configuration config = desktop.getWebApp().getConfiguration();
		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		final UiVisualizer uv = (UiVisualizer) execCtrl.getVisualizer();
		try {
			final List<Throwable> errs = new LinkedList<Throwable>();
			Event event = nextEvent(uv);
			do {
				for (; event != null; event = nextEvent(uv)) {
					try {
						process(desktop, event);
					} catch (Throwable ex) {
						handleError(ex, uv, errs);
					}
				}
				resumeAll(desktop, uv, null);
			} while ((event = nextEvent(uv)) != null);

			desktopCtrl.piggyResponse(getResponses(exec, uv, errs, true), false);

			desktopCtrl.invokeExecutionCleanups(exec, null, errs);
			config.invokeExecutionCleanups(exec, null, errs);
		} catch (Throwable ex) {
			final List<Throwable> errs = new LinkedList<Throwable>();
			errs.add(ex);

			desktopCtrl.invokeExecutionCleanups(exec, null, errs);
			config.invokeExecutionCleanups(exec, null, errs);

			if (!errs.isEmpty()) {
				ex = errs.get(0);
				if (ex instanceof IOException)
					throw (IOException) ex;
				throw UiException.Aide.wrap(ex);
			}
		} finally {
			doDeactivate(exec);
		}
	}

	public void execUpdate(Execution exec, List<AuRequest> requests, AuWriter out) throws IOException {
		if (requests == null)
			throw new IllegalArgumentException();
		//		assert ExecutionsCtrl.getCurrentCtrl() == null:
		//			"Impossible to re-activate for update: old="+ExecutionsCtrl.getCurrentCtrl()+", new="+exec;

		final Desktop desktop = exec.getDesktop();
		final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
		final Configuration config = desktop.getWebApp().getConfiguration();

		final PerformanceMeter pfmeter = config.getPerformanceMeter();
		long startTime = 0;
		if (pfmeter != null) {
			startTime = System.currentTimeMillis();
			//snapshot time since activate might take time
			meterAuClientComplete(pfmeter, exec);
		}

		final Object[] resultOfRepeat = new Object[1];
		final UiVisualizer uv = doActivate(exec, true, false, resultOfRepeat, -1);
		if (resultOfRepeat[0] != null) {
			out.resend(resultOfRepeat[0]);
			doDeactivate(exec);
			return;
		}

		final Monitor monitor = config.getMonitor();
		if (monitor != null) {
			try {
				monitor.beforeUpdate(desktop, requests);
			} catch (Throwable ex) {
				log.error("", ex);
			}
		}

		final String pfReqId = pfmeter != null ? meterAuStart(pfmeter, exec, startTime) : null;
		Collection<String> doneReqIds = null; //request IDs that have been processed
		AbortingReason abrn = null;
		try {
			final RequestQueue rque = desktopCtrl.getRequestQueue();
			rque.addRequests(requests);

			config.invokeExecutionInits(exec, null);
			desktopCtrl.invokeExecutionInits(exec, null);

			if (pfReqId != null)
				rque.addPerfRequestId(pfReqId);

			final List<Throwable> errs = new LinkedList<Throwable>();
			final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
			//Process all; ignore getMaxProcessTime();
			//we cannot handle them partially since UUID might be recycled
			for (AuRequest request; (request = rque.nextRequest()) != null;) {
				//Cycle 1: Process one request
				//Don't process more such that requests will be queued
				//and we have the chance to optimize them
				execCtrl.setCurrentPage(request.getPage());
				try {
					((DesktopCtrl) desktop).service(request, !errs.isEmpty());
				} catch (Throwable ex) {
					handleError(ex, uv, errs);
					//we don't skip request to avoid mismatch between c/s
				}

				//Cycle 2: Process any pending events posted by components
				Event event = nextEvent(uv);
				do {
					for (; event != null; event = nextEvent(uv)) {
						try {
							process(desktop, event);
						} catch (Throwable ex) {
							handleError(ex, uv, errs);
						}
					}

					resumeAll(desktop, uv, errs);
				} while ((event = nextEvent(uv)) != null);
			}

			//Cycle 2a: Handle aborting reason
			abrn = uv != null ? uv.getAbortingReason() : null;
			if (abrn != null)
				abrn.execute(); //always execute even if !isAborting

			//Cycle 3: Generate output
			final List<AuResponse> responses = getResponses(exec, uv, errs, true);

			doneReqIds = rque.clearPerfRequestIds();

			final List<AuResponse> prs = desktopCtrl.piggyResponse(null, true);
			if (prs != null)
				responses.addAll(0, prs);

			out.writeResponseId(desktopCtrl.getResponseId(true));
			out.write(mergeResponses(responses));

			//			if (log.isDebugEnabled())
			//				if (responses.size() < 5 || log.finerable()) log.finer("Responses: "+responses);
			//				else log.debug("Responses: "+responses.subList(0, 5)+"...");

			final String seqId = ((ExecutionCtrl) exec).getRequestId();
			if (seqId != null)
				desktopCtrl.responseSent(seqId, out.complete());

			desktopCtrl.invokeExecutionCleanups(exec, null, errs);
			config.invokeExecutionCleanups(exec, null, errs);
		} catch (Throwable ex) {
			final List<Throwable> errs = new LinkedList<Throwable>();
			errs.add(ex);

			desktopCtrl.invokeExecutionCleanups(exec, null, errs);
			config.invokeExecutionCleanups(exec, null, errs);

			if (!errs.isEmpty()) {
				ex = errs.get(0);
				if (ex instanceof IOException)
					throw (IOException) ex;
				throw UiException.Aide.wrap(ex);
			}
		} finally {
			if (abrn != null) {
				try {
					abrn.finish();
				} catch (Throwable t) {
					log.warn("", t);
				}
			}
			if (monitor != null) {
				try {
					monitor.afterUpdate(desktop);
				} catch (Throwable ex) {
					log.error("", ex);
				}
			}

			doDeactivate(exec);

			if (pfmeter != null && doneReqIds != null)
				meterAuServerComplete(pfmeter, doneReqIds, exec);
		}
	}

	private List<AuResponse> mergeResponses(List<AuResponse> responses) {
		if (responses.size() >= 2) { // worth merging
			List<AuSetAttribute> bulk = new LinkedList<>();
			ListIterator<AuResponse> iterator = responses.listIterator();
			Object previousDepends = null, currentDepends = null;
			while (iterator.hasNext()) {
				AuResponse resp = iterator.next();
				boolean isSetAttr = resp instanceof AuSetAttribute;
				currentDepends = isSetAttr ? resp.getDepends() : null;
				if (previousDepends != currentDepends) {
					if (!bulk.isEmpty()) {
						iterator.previous();
						iterator.add(bulk.size() == 1
								? bulk.get(0)
								: new AuSetAttributes((Component) previousDepends,
										bulk.toArray(new AuSetAttribute[0])));
						iterator.next();
						bulk.clear();
					}
				}
				if (isSetAttr) {
					bulk.add((AuSetAttribute) resp);
					iterator.remove();
				}
				previousDepends = currentDepends;
			}
			if (!bulk.isEmpty()) {
				responses.add(bulk.size() == 1
						? bulk.get(0)
						: new AuSetAttributes((Component) currentDepends, bulk.toArray(new AuSetAttribute[0])));
				bulk.clear();
			}
		}
		return responses;
	}

	public Object startUpdate(Execution exec) throws IOException {
		final Desktop desktop = exec.getDesktop();
		UiVisualizer uv = doActivate(exec, true, false, null, -1);
		desktop.getWebApp().getConfiguration().invokeExecutionInits(exec, null);
		((DesktopCtrl) desktop).invokeExecutionInits(exec, null);
		return new UpdateInfo(uv);
	}

	public JSONArray finishUpdate(Object ctx) throws IOException {
		return finishUpdate(ctx, new LinkedList<Throwable>());
	}

	// zk 10
	public JSONArray finishUpdate(Object ctx, List<Throwable> errs) throws IOException {
		final UpdateInfo ui = (UpdateInfo) ctx;
		final Execution exec = ui.uv.getExecution();
		final Desktop desktop = exec.getDesktop();

		//1. process events
		Event event = nextEvent(ui.uv);
		do {
			for (; event != null; event = nextEvent(ui.uv)) {
				try {
					process(desktop, event);
				} catch (Throwable ex) {
					handleError(ex, ui.uv, errs);
				}
			}

			resumeAll(desktop, ui.uv, errs);
		} while ((event = nextEvent(ui.uv)) != null);

		//2. Handle aborting reason
		ui.abrn = ui.uv.getAbortingReason();
		if (ui.abrn != null)
			ui.abrn.execute(); //always execute even if !isAborting

		//3. Retrieve responses
		final List<AuResponse> responses = getResponses(exec, ui.uv, errs, false);

		final JSONArray rs = new JSONArray();
		for (AuResponse response : responses)
			rs.add(AuWriters.toJSON(response));
		return rs;
	}

	public void closeUpdate(Object ctx) throws IOException {
		final UpdateInfo ui = (UpdateInfo) ctx;
		final Execution exec = ui.uv.getExecution();

		final Desktop desktop = exec.getDesktop();
		((DesktopCtrl) desktop).invokeExecutionCleanups(exec, null, null);
		desktop.getWebApp().getConfiguration().invokeExecutionCleanups(exec, null, null);

		if (ui.abrn != null) {
			try {
				ui.abrn.finish();
			} catch (Throwable t) {
				log.warn("", t);
			}
		}

		doDeactivate(exec);
	}

	private static class UpdateInfo {
		private final UiVisualizer uv;
		private AbortingReason abrn;

		private UpdateInfo(UiVisualizer uv) {
			this.uv = uv;
		}
	}

	/** Handles each error. The errors will be queued to the errs list
	 * and processed later by {@link #visualizeErrors}.
	 */
	private static final void handleError(Throwable ex, UiVisualizer uv, List<Throwable> errs) {
		final Throwable t = Exceptions.findCause(ex, Expectable.class);
		if (t == null) {
			String requestPath = uv.getExecution().getDesktop().getRequestPath();
			if (ex instanceof org.xml.sax.SAXException
					|| ex instanceof org.zkoss.zk.ui.metainfo.PropertyNotFoundException)
				log.error("{} at [{}]", Exceptions.getMessage(ex), requestPath);
			else
				log.error("at [{}]", requestPath, ex); //Briefly(ex);
		} else {
			ex = t;
			if (log.isDebugEnabled()) {
				String requestPath = uv.getExecution().getDesktop().getRequestPath();
				log.debug("at [{}]", requestPath, Exceptions.getRealCause(ex));
			}
		}

		if (ex instanceof WrongValueException) {
			WrongValueException wve = (WrongValueException) ex;
			final Component comp = wve.getComponent();
			if (comp != null) {
				wve = ((ComponentCtrl) comp).onWrongValue(wve);
				if (wve != null) {
					Component c = wve.getComponent();
					if (c == null)
						c = comp;
					uv.addResponse(new AuWrongValue(c, Exceptions.getMessage(wve)));
				}
				return;
			}
		} else if (ex instanceof WrongValuesException) {
			final WrongValueException[] wves = ((WrongValuesException) ex).getWrongValueExceptions();
			final LinkedList<String> infs = new LinkedList<String>();
			for (WrongValueException wve1 : wves) {
				final Component comp = wve1.getComponent();
				if (comp != null) {
					WrongValueException wve = ((ComponentCtrl) comp).onWrongValue(wve1);
					if (wve != null) {
						Component c = wve.getComponent();
						if (c == null)
							c = comp;
						infs.add(c.getUuid());
						infs.add(Exceptions.getMessage(wve));
					}
				}
			}
			uv.addResponse(new AuWrongValue(infs.toArray(new String[infs.size()])));
			return;
		}

		errs.add(ex);
	}

	/** Returns the list of response of the given execution.
	 * @since bAfterRender whether to call back {@link #afterRender}
	 * for the attached components (topmost only)
	 */
	private final List<AuResponse> getResponses(Execution exec, UiVisualizer uv, List<Throwable> errs,
			boolean bAfterRender) {
		List<AuResponse> responses;
		try {
			//Note: we have to call visualizeErrors before uv.getResponses,
			//since it might create/update components
			if (!errs.isEmpty())
				visualizeErrors(exec, uv, errs);

			final List<Component> renderedComps = bAfterRender ? new LinkedList<Component>() : null;
			responses = uv.getResponses(renderedComps);
			if (bAfterRender)
				afterRenderComponents(renderedComps);
		} catch (Throwable ex) {
			responses = new LinkedList<AuResponse>();
			responses.add(new AuAlert(Exceptions.getMessage(ex)));

			log.error("", ex);
		}
		return responses;
	}

	/** Post-process the errors to represent them to the user.
	 * Note: errs must be non-empty
	 */
	private final void visualizeErrors(Execution exec, UiVisualizer uv, List<Throwable> errs) {
		final StringBuffer sb = new StringBuffer(128);
		for (Throwable t : errs) {
			if (sb.length() > 0)
				sb.append('\n');
			sb.append(Exceptions.getMessage(t));
		}
		final String msg = sb.toString();

		final Throwable err = errs.get(0);
		final Desktop desktop = exec.getDesktop();
		final Configuration config = desktop.getWebApp().getConfiguration();
		final String location = config.getErrorPage(desktop.getDeviceType(), err);
		if (location != null) {
			try {
				exec.setAttribute("javax.servlet.error.message", msg);
				exec.setAttribute("javax.servlet.error.exception_list", errs);
				exec.setAttribute("javax.servlet.error.exception", err);
				exec.setAttribute("javax.servlet.error.exception_type", err.getClass());
				exec.setAttribute("javax.servlet.error.status_code", new Integer(500));
				exec.setAttribute("javax.servlet.error.error_page", location);

				//Future: consider to go thru UiFactory for the richlet
				//for the error page.
				//Challenge: how to call UiFactory.isRichlet
				final Richlet richlet = config.getRichletByPath(location);
				if (richlet != null)
					richlet.service(((ExecutionCtrl) exec).getCurrentPage());
				else
					exec.createComponents(location, null, null);

				//process pending events
				//the execution is aborted if an exception is thrown
				Event event = nextEvent(uv);
				do {
					for (; event != null; event = nextEvent(uv)) {
						try {
							process(desktop, event);
						} catch (SuspendNotAllowedException ex) {
							//ignore it (possible and reasonable)
						}
					}
					resumeAll(desktop, uv, null);
				} while ((event = nextEvent(uv)) != null);
				return; //done
			} catch (Throwable ex) {
				log.error("Unable to generate custom error page, " + location, ex);
			} finally {
				// Bug ZK-1144 in JBoss
				exec.removeAttribute("javax.servlet.error.message");
				exec.removeAttribute("javax.servlet.error.exception_list");
				exec.removeAttribute("javax.servlet.error.exception");
				exec.removeAttribute("javax.servlet.error.exception_type");
				exec.removeAttribute("javax.servlet.error.status_code");
				exec.removeAttribute("javax.servlet.error.error_page");
			}
		}

		uv.addResponse(new AuAlert(msg, true)); //default handling
	}

	/** Processing the event and stores result into UiVisualizer. */
	private void process(Desktop desktop, Event event) {
		//		if (log.finable()) log.finer("Processing event: "+event);

		final Component comp;
		if (event instanceof ProxyEvent) {
			final ProxyEvent pe = (ProxyEvent) event;
			comp = pe.getRealTarget();
			event = pe.getProxiedEvent();
		} else {
			comp = event.getTarget();
		}
		if (comp != null) {
			processEvent(desktop, comp, event);
		} else {
			//since an event might change the page/desktop/component relation,
			//we copy roots first
			final List<Component> roots = new LinkedList<Component>();
			for (Page page : desktop.getPages()) {
				roots.addAll(page.getRoots());
			}
			for (Component c : roots) {
				if (c.getPage() != null) //might be removed, so check first
					processEvent(desktop, c, event);
			}
		}
	}

	public void wait(Object mutex) throws InterruptedException, SuspendNotAllowedException {
		if (mutex == null)
			throw new IllegalArgumentException("null mutex");

		final Thread thd = Thread.currentThread();
		if (!(thd instanceof EventProcessingThreadImpl))
			throw new UiException("This method can be called only in an event listener, not in paging loading.");
		//		if (log.finerable()) log.finer("Suspend "+thd+" on "+mutex);

		final EventProcessingThreadImpl evtthd = (EventProcessingThreadImpl) thd;
		evtthd.newEventThreadSuspends(mutex);
		//it may throw an exception, so process it before updating _suspended

		final Execution exec = Executions.getCurrent();
		final Desktop desktop = exec.getDesktop();

		incSuspended();

		Map<Object, List<EventProcessingThreadImpl>> map;
		synchronized (_suspended) {
			map = _suspended.get(desktop);
			if (map == null)
				_suspended.put(desktop, map = new IdentityHashMap<Object, List<EventProcessingThreadImpl>>(4));
			//note: we have to use IdentityHashMap because user might
			//use Integer or so as mutex
		}
		synchronized (map) {
			List<EventProcessingThreadImpl> list = map.get(mutex);
			if (list == null)
				map.put(mutex, list = new LinkedList<EventProcessingThreadImpl>());
			list.add(evtthd);
		}

		try {
			EventProcessingThreadImpl.doSuspend(mutex);
		} catch (Throwable ex) {
			//error recover
			synchronized (map) {
				final List<EventProcessingThreadImpl> list = map.get(mutex);
				if (list != null) {
					list.remove(evtthd);
					if (list.isEmpty())
						map.remove(mutex);
				}
			}

			if (ex instanceof InterruptedException)
				throw (InterruptedException) ex;
			throw UiException.Aide.wrap(ex, "Unable to suspend " + evtthd);
		} finally {
			decSuspended();
		}
	}

	private void incSuspended() {
		final int v = _wapp.getConfiguration().getMaxSuspendedThreads();
		synchronized (this) {
			if (v >= 0 && _suspCnt >= v)
				throw new SuspendNotAllowedException(MZk.TOO_MANY_SUSPENDED);
			++_suspCnt;
		}
	}

	private void decSuspended() {
		synchronized (this) {
			--_suspCnt;
		}
	}

	public void notify(Object mutex) {
		notify(Executions.getCurrent().getDesktop(), mutex);
	}

	public void notify(Desktop desktop, Object mutex) {
		if (desktop == null || mutex == null)
			throw new IllegalArgumentException("desktop and mutex cannot be null");

		final Map<Object, List<EventProcessingThreadImpl>> map;
		synchronized (_suspended) {
			map = _suspended.get(desktop);
		}
		if (map == null)
			return; //nothing to notify

		final EventProcessingThreadImpl evtthd;
		synchronized (map) {
			final List<EventProcessingThreadImpl> list = map.get(mutex);
			if (list == null)
				return; //nothing to notify

			//Note: list is never empty
			evtthd = list.remove(0);
			if (list.isEmpty())
				map.remove(mutex); //clean up
		}
		addResumed(desktop, evtthd);
	}

	public void notifyAll(Object mutex) {
		final Execution exec = Executions.getCurrent();
		if (exec == null)
			throw new UiException("resume can be called only in processing a request");
		notifyAll(exec.getDesktop(), mutex);
	}

	public void notifyAll(Desktop desktop, Object mutex) {
		if (desktop == null || mutex == null)
			throw new IllegalArgumentException("desktop and mutex cannot be null");

		final Map<Object, List<EventProcessingThreadImpl>> map;
		synchronized (_suspended) {
			map = _suspended.get(desktop);
		}
		if (map == null)
			return; //nothing to notify

		final List<EventProcessingThreadImpl> list;
		synchronized (map) {
			list = map.remove(mutex);
		}
		if (list == null)
			return; //nothing to notify

		for (EventProcessingThreadImpl thread : list)
			addResumed(desktop, thread);
	}

	/** Adds to _resumed */
	private void addResumed(Desktop desktop, EventProcessingThreadImpl evtthd) {
		//		if (log.finerable()) log.finer("Ready to resume "+evtthd);

		List<EventProcessingThreadImpl> list;
		synchronized (_resumed) {
			list = _resumed.get(desktop);
			if (list == null)
				_resumed.put(desktop, list = new LinkedList<EventProcessingThreadImpl>());
		}
		synchronized (list) {
			list.add(evtthd);
		}
	}

	/** Does the real resume.
	 * <p>Note 1: the current thread will wait until the resumed threads, if any, complete
	 * <p>Note 2: {@link #resume} only puts a thread into a resume queue in execution.
	 */
	private void resumeAll(Desktop desktop, UiVisualizer uv, List<Throwable> errs) {
		//We have to loop because a resumed thread might resume others
		while (!_resumed.isEmpty()) { //no need to sync (better performance)
			final List<EventProcessingThreadImpl> list;
			synchronized (_resumed) {
				list = _resumed.remove(desktop);
				if (list == null)
					return; //nothing to resume; done
			}

			synchronized (list) {
				for (EventProcessingThreadImpl evtthd : list) {
					if (uv.isAborting()) {
						evtthd.ceaseSilently("Resume aborted");
					} else {
						//						if (log.finerable()) log.finer("Resume "+evtthd);
						try {
							if (evtthd.doResume()) //wait it complete or suspend again
								recycleEventThread(evtthd); //completed
						} catch (Throwable ex) {
							recycleEventThread(evtthd);
							if (errs == null) {
								log.error("Unable to resume " + evtthd, ex);
								throw UiException.Aide.wrap(ex);
							}
							handleError(ex, uv, errs);
						}
					}
				}
			}
		}
	}

	/** Process an event. */
	private void processEvent(Desktop desktop, Component comp, Event event) {
		final Configuration config = desktop.getWebApp().getConfiguration();
		if (config.isEventThreadEnabled()) {
			EventProcessingThreadImpl evtthd = null;
			synchronized (_idles) {
				while (!_idles.isEmpty() && evtthd == null) {
					evtthd = _idles.remove(0);
					if (evtthd.isCeased()) //just in case
						evtthd = null;
				}
			}

			if (evtthd == null)
				evtthd = new EventProcessingThreadImpl();

			try {
				if (evtthd.processEvent(desktop, comp, event))
					recycleEventThread(evtthd);
			} catch (Throwable ex) {
				recycleEventThread(evtthd);
				throw UiException.Aide.wrap(ex);
			}
		} else { //event thread disabled
			//Note: we don't need to call proc.setup() and cleanup(),
			//since they are in the same thread
			EventProcessor proc = new EventProcessor(desktop, comp, event);
			//Note: it also checks the correctness
			List<EventThreadCleanup> cleanups = null;
			List<Throwable> errs = null;
			try {
				final List<EventThreadInit> inits = config.newEventThreadInits(comp, event);
				EventProcessor.inEventListener(true);
				if (config.invokeEventThreadInits(inits, comp, event)) //false measn ignore
					proc.process();
			} catch (Throwable ex) {
				errs = new LinkedList<Throwable>();
				errs.add(ex);
				cleanups = config.newEventThreadCleanups(comp, event, errs, false);

				if (!errs.isEmpty())
					throw UiException.Aide.wrap(errs.get(0));
			} finally {
				EventProcessor.inEventListener(false);
				if (errs == null) //not cleanup yet
					cleanups = config.newEventThreadCleanups(comp, event, null, false);
				config.invokeEventThreadCompletes(cleanups, comp, event, errs, false);
			}
		}
	}

	private void recycleEventThread(EventProcessingThreadImpl evtthd) {
		if (!evtthd.isCeased()) {
			if (evtthd.isIdle()) {
				final int max = _wapp.getConfiguration().getMaxSpareThreads();
				synchronized (_idles) {
					if (max < 0 || _idles.size() < max) {
						_idles.add(evtthd); //return to pool
						return; //done
					}
				}
			}
			evtthd.ceaseSilently("Recycled");
		}
	}

	public void activate(Execution exec) {
		activate(exec, -1);
	}

	public boolean activate(Execution exec, int timeout) {
		//		assert ExecutionsCtrl.getCurrentCtrl() == null:
		//			"Impossible to re-activate for update: old="+ExecutionsCtrl.getCurrentCtrl()+", new="+exec;
		return doActivate(exec, false, false, null, timeout) != null;
	}

	public void deactivate(Execution exec) {
		doDeactivate(exec);
	}

	//-- Common private utilities --//
	/** Activates the specified execution.
	 *
	 * @param asyncupd whether it is for asynchronous update.
	 * Note: it doesn't support if both asyncupd and recovering are true.
	 * @param recovering whether it is in recovering, i.e.,
	 * cause by {@link FailoverManager#recover}.
	 * If true, the requests argument must be null.
	 * @param resultOfRepeat a single element array to return a value, or null
	 * if it is not called by execUpdate.
	 * If a non-null value is assigned to the first element of the array,
	 * it means it is a repeated request, and the caller shall return
	 * the result directly without processing the request.
	 * @param timeout how many milliseconds to wait before timeout.
	 * If non-negative and it waits more than it before granted, null is turned
	 * to indicate failure.
	 * @return the visualizer once the execution is granted, or null
	 * if timeout is specified and it takes longer than the given value.
	 */
	private static UiVisualizer doActivate(Execution exec, boolean asyncupd, boolean recovering,
			Object[] resultOfRepeat, int timeout) {
		if (Executions.getCurrent() != null)
			throw new IllegalStateException("Use doReactivate instead");
		//		assert !recovering || !asyncupd; //Not support both asyncupd and recovering are true yet

		final Desktop desktop = exec.getDesktop();
		final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
		final Session sess = desktop.getSession();
		final ExecutionMonitor execmon = desktop.getWebApp().getConfiguration().getExecutionMonitor();
		final String seqId = resultOfRepeat != null ? ((ExecutionCtrl) exec).getRequestId() : null;
		//		if (log.finerable()) log.finer("Activating "+desktop);

		//lock desktop
		final UiVisualizer uv;
		final Object uvlock = desktopCtrl.getActivationLock();
		final int tmout = timeout >= 0 ? timeout : getRetryTimeout();
		synchronized (uvlock) {
			for (boolean tried = false;;) {
				if (!desktop.isAlive())
					throw new org.zkoss.zk.ui.DesktopUnavailableException(
							"Unable to activate destroyed desktop, " + desktop);

				final Visualizer old = desktopCtrl.getVisualizer();
				if (old == null)
					break; //grantable
				if (tried) {
					if (timeout >= 0)
						return null; //failed
					if (_abortSpecified)
						throw new ActivationTimeoutException("Aborted activation because of timeout, " + tmout + "ms.");
				}

				if (seqId != null) {
					final String oldSeqId = ((ExecutionCtrl) old.getExecution()).getRequestId();
					if (oldSeqId != null && !oldSeqId.equals(seqId))
						throw new RequestOutOfSequenceException(seqId, oldSeqId);
				}

				if (execmon != null)
					execmon.executionWait(exec, desktop);

				try {
					uvlock.wait(tmout);
					tried = true;
				} catch (InterruptedException ex) {
					if (execmon != null)
						execmon.executionAbort(exec, desktop, ex);
					throw UiException.Aide.wrap(ex);
				}
			}

			//grant
			desktopCtrl.setVisualizer(uv = new UiVisualizer(exec, asyncupd, recovering));
			desktopCtrl.setExecution(exec);
		}

		//		if (log.finerable()) log.finer("Activated "+desktop);

		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		ExecutionsCtrl.setCurrent(exec);
		try {
			execCtrl.onActivate();
		} catch (Throwable ex) { //just in case
			ExecutionsCtrl.setCurrent(null);
			synchronized (uvlock) {
				desktopCtrl.setVisualizer(null);
				desktopCtrl.setExecution(null);
				uvlock.notify(); //wake up pending threads
			}
			if (execmon != null)
				execmon.executionAbort(exec, desktop, ex);
			throw UiException.Aide.wrap(ex);
		}

		if (seqId != null) {
			if (log.isDebugEnabled()) {
				final Object req = exec.getNativeRequest();
				log.debug("replicate request, SID: {}{}", seqId,
						req instanceof ServletRequest
								? "\n" + Servlets.getDetail(
								(ServletRequest) req)
								: "");
			}
			resultOfRepeat[0] = desktopCtrl.getLastResponse(seqId);
		}

		if (execmon != null)
			execmon.executionActivate(exec, desktop);
		return uv;
	}

	private static volatile Integer _retryTimeout, _destroyTimeout;
	private static boolean _abortSpecified;

	private static final int getRetryTimeout() {
		if (_retryTimeout == null) {
			int v = 0;
			final String s = Library.getProperty(Attributes.ACTIVATE_RETRY_DELAY);
			if (s != null) {
				try {
					v = Integer.parseInt(s);
					if (v > 0 && "true".equals(Library.getProperty(Attributes.ACTIVATE_RETRY_ABORT)))
						_abortSpecified = true;
				} catch (Throwable t) {
					// expected
				}
			}
			_retryTimeout = new Integer(v > 0 ? v : 120 * 1000);
		}
		return _retryTimeout.intValue();
	}

	private static final int getDestroyTimeout() {
		if (_destroyTimeout == null) {
			int v = 0;
			final String s = Library.getProperty("org.zkoss.zk.ui.activate.wait.destroy.timeout");
			if (s != null) {
				try {
					v = Integer.parseInt(s);
				} catch (Throwable t) {
					// expected
				}
			}
			_destroyTimeout = new Integer(v > 0 ? v : 20 * 1000); //20 sec
		}
		return _destroyTimeout.intValue();
	}

	/** Returns whether the desktop is being recovered.
	 */
	private static final boolean isRecovering(Desktop desktop) {
		final Execution exec = desktop.getExecution();
		return exec != null && ((ExecutionCtrl) exec).isRecovering();
	}

	/** Deactivates the execution. */
	private static final void doDeactivate(Execution exec) {
		//		if (log.finerable()) log.finer("Deactivating "+desktop);

		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		final Desktop desktop = exec.getDesktop();
		final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
		try {
			try {
				execCtrl.onBeforeDeactivate();
			} catch (Throwable ex) {
				log.warn("Failed to be deactiving", ex);
			}
			//Unlock desktop
			final Object uvlock = desktopCtrl.getActivationLock();
			synchronized (uvlock) {
				desktopCtrl.setVisualizer(null);
				desktopCtrl.setExecution(null);
				uvlock.notify(); //wake up doActivate's wait
			}
		} finally {
			try {
				execCtrl.onDeactivate();
			} catch (Throwable ex) {
				log.warn("Failed to deactive", ex);
			}
			ExecutionsCtrl.setCurrent(null);
			execCtrl.setCurrentPage(null);

			final ExecutionMonitor execmon = desktop.getWebApp().getConfiguration().getExecutionMonitor();
			if (execmon != null)
				execmon.executionDeactivate(exec, desktop);
		}

		final SessionCtrl sessCtrl = (SessionCtrl) desktop.getSession();

		if (sessCtrl.isInvalidated()) {

			// Fix ZK-3729, only invalidate if there is no working desktop in the session.
			DesktopCache desktopCache = sessCtrl.getDesktopCache();
			boolean hasWorkingDesktop = false;
			if (desktopCache != null) {
				List<Desktop> allDesktops = desktopCache.getAllDesktops();

				for (Desktop d : allDesktops) {
					if (d != desktop && d.getExecution() != null) {
						hasWorkingDesktop = true;
						break;
					}
				}
			}

			if (!hasWorkingDesktop)
				sessCtrl.invalidateNow();
		}
	}

	/** Re-activates for another execution. It is callable only for
	 * creating new page (execNewPage). It is not allowed for async-update.
	 * <p>Note: doActivate cannot handle reactivation. In other words,
	 * the caller has to detect which method to use.
	 */
	private static UiVisualizer doReactivate(Execution curExec, UiVisualizer olduv) {
		final Desktop desktop = curExec.getDesktop();
		final Session sess = desktop.getSession();
		//		if (log.finerable()) log.finer("Re-activating "+desktop);

		assert olduv.getExecution().getDesktop() == desktop : "old dt: " + olduv.getExecution().getDesktop() + ", new:"
				+ desktop;

		final UiVisualizer uv = new UiVisualizer(olduv, curExec);
		final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
		desktopCtrl.setVisualizer(uv);
		desktopCtrl.setExecution(curExec);

		final ExecutionCtrl curCtrl = (ExecutionCtrl) curExec;
		ExecutionsCtrl.setCurrent(curExec);
		try {
			curCtrl.onActivate();
		} catch (Throwable ex) { //just in case
			ExecutionsCtrl.setCurrent(olduv.getExecution());
			desktopCtrl.setVisualizer(olduv);
			desktopCtrl.setExecution(olduv.getExecution());
			throw UiException.Aide.wrap(ex);
		}
		return uv;
	}

	/** De-reactivated exec. Work with {@link #doReactivate}.
	 */
	private static void doDereactivate(Execution curExec, UiVisualizer olduv) {
		if (olduv == null)
			throw new IllegalArgumentException("null");

		final ExecutionCtrl curCtrl = (ExecutionCtrl) curExec;
		final Execution oldexec = olduv.getExecution();
		try {
			final Desktop desktop = curExec.getDesktop();
			//		if (log.finerable()) log.finer("Deactivating "+desktop);

			try {
				curCtrl.onDeactivate();
			} catch (Throwable ex) {
				log.warn("Failed to deactive", ex);
			}

			final DesktopCtrl desktopCtrl = (DesktopCtrl) desktop;
			desktopCtrl.setVisualizer(olduv);
			desktopCtrl.setExecution(oldexec);
		} finally {
			ExecutionsCtrl.setCurrent(oldexec);
			curCtrl.setCurrentPage(null);
		}
	}

	//Handling Native Component//
	/** Sets the prolog of the specified native component.
	 */
	private static final void setProlog(CreateInfo ci, Component comp, NativeInfo compInfo) {
		final Native nc = (Native) comp;
		final Native.Helper helper = nc.getHelper();
		StringBuffer sb = null;
		final List<NodeInfo> prokids = compInfo.getPrologChildren();
		if (!prokids.isEmpty()) {
			sb = new StringBuffer(256);
			getNativeContent(ci, sb, comp, prokids, helper);
		}

		final NativeInfo splitInfo = compInfo.getSplitChild();
		if (splitInfo != null && splitInfo.isEffective(comp)) {
			if (sb == null)
				sb = new StringBuffer(256);
			getNativeFirstHalf(ci, sb, comp, splitInfo, helper);
		}

		if (sb != null && sb.length() > 0)
			nc.setPrologContent(sb.insert(0, nc.getPrologContent()).toString());
	}

	/** Sets the epilog of the specified native component.
	 * @param comp the native component
	 */
	private static final void setEpilog(CreateInfo ci, Component comp, NativeInfo compInfo) {
		final Native nc = (Native) comp;
		final Native.Helper helper = nc.getHelper();
		StringBuffer sb = null;
		final NativeInfo splitInfo = compInfo.getSplitChild();
		if (splitInfo != null && splitInfo.isEffective(comp)) {
			sb = new StringBuffer(256);
			getNativeSecondHalf(ci, sb, comp, splitInfo, helper);
		}

		final List<NodeInfo> epikids = compInfo.getEpilogChildren();
		if (!epikids.isEmpty()) {
			if (sb == null)
				sb = new StringBuffer(256);
			getNativeContent(ci, sb, comp, epikids, helper);
		}

		if (sb != null && sb.length() > 0)
			nc.setEpilogContent(sb.append(nc.getEpilogContent()).toString());
	}

	public String getNativeContent(Component comp, List<NodeInfo> children, Native.Helper helper) {
		final StringBuffer sb = new StringBuffer(256);
		getNativeContent(
				new CreateInfo(((WebAppCtrl) _wapp).getUiFactory(), Executions.getCurrent(), comp.getPage(), null), sb,
				comp, children, helper);
		return sb.toString();
	}

	/**
	 * @param comp the native component
	 */
	private static final void getNativeContent(CreateInfo ci, StringBuffer sb, Component comp, List<NodeInfo> children,
			Native.Helper helper) {
		for (NodeInfo meta : children) {
			if (meta instanceof NativeInfo) {
				final NativeInfo childInfo = (NativeInfo) meta;
				final ForEach forEach = childInfo.resolveForEach(ci.page, comp);
				if (forEach == null) {
					if (childInfo.isEffective(comp)) {
						getNativeFirstHalf(ci, sb, comp, childInfo, helper);
						getNativeSecondHalf(ci, sb, comp, childInfo, helper);
					}
				} else {
					while (forEach.next()) {
						if (childInfo.isEffective(comp)) {
							getNativeFirstHalf(ci, sb, comp, childInfo, helper);
							getNativeSecondHalf(ci, sb, comp, childInfo, helper);
						}
					}
				}
			} else if (meta instanceof TextInfo) {
				final String s = ((TextInfo) meta).getValue(comp);
				if (s != null)
					helper.appendText(sb, s);
			} else if (meta instanceof ZkInfo) {
				ZkInfo zkInfo = (ZkInfo) meta;
				if (zkInfo.withSwitch())
					throw new UnsupportedOperationException("<zk switch> in native not allowed yet");

				final ForEach forEach = zkInfo.resolveForEach(ci.page, comp);
				if (forEach == null) {
					if (isEffective(zkInfo, ci.page, comp)) {
						getNativeContent(ci, sb, comp, zkInfo.getChildren(), helper);
					}
				} else {
					while (forEach.next())
						if (isEffective(zkInfo, ci.page, comp))
							getNativeContent(ci, sb, comp, zkInfo.getChildren(), helper);
				}
			} else {
				execNonComponent(ci, comp, meta);
			}
		}
	}

	/** Before calling this method, childInfo.isEffective must be examined
	 */
	private static final void getNativeFirstHalf(CreateInfo ci, StringBuffer sb, Component comp, NativeInfo childInfo,
			Native.Helper helper) {
		helper.getFirstHalf(sb, childInfo.getTag(), evalProperties(comp, childInfo.getProperties()),
				childInfo.getDeclaredNamespaces());

		final List<NodeInfo> prokids = childInfo.getPrologChildren();
		if (!prokids.isEmpty())
			getNativeContent(ci, sb, comp, prokids, helper);

		final NativeInfo splitInfo = childInfo.getSplitChild();
		if (splitInfo != null && splitInfo.isEffective(comp))
			getNativeFirstHalf(ci, sb, comp, splitInfo, helper); //recursive
	}

	/** Before calling this method, childInfo.isEffective must be examined
	 */
	private static final void getNativeSecondHalf(CreateInfo ci, StringBuffer sb, Component comp, NativeInfo childInfo,
			Native.Helper helper) {
		final NativeInfo splitInfo = childInfo.getSplitChild();
		if (splitInfo != null && splitInfo.isEffective(comp))
			getNativeSecondHalf(ci, sb, comp, splitInfo, helper); //recursive

		final List<NodeInfo> epikids = childInfo.getEpilogChildren();
		if (!epikids.isEmpty())
			getNativeContent(ci, sb, comp, epikids, helper);

		helper.getSecondHalf(sb, childInfo.getTag());
	}

	/** Returns a map of properties, (String name, String value).
	 */
	private static final Map<String, Object> evalProperties(Component comp, List<Property> props) {
		if (props == null || props.isEmpty())
			return Collections.emptyMap();

		final Map<String, Object> map = new LinkedHashMap<String, Object>(props.size() * 2);
		for (Property prop : props) {
			if (prop.isEffective(comp))
				map.put(prop.getName(), Classes.coerce(String.class, prop.getValue(comp)));
		}
		return map;
	}

	//Supporting Classes//
	private static class TemplateImpl implements Template, TemplateCtrl, java.io.Serializable {
		private final TemplateInfo _tempInfo;
		private final Map<String, Object> _params;
		private final String _src;

		private TemplateImpl(TemplateInfo tempInfo, Component comp) {
			_tempInfo = tempInfo;
			_params = tempInfo.resolveParameters(comp);
			_src = tempInfo.getSrc(comp);
		}

		public Component[] create(Component parent, Component insertBefore, VariableResolver resolver,
				Composer composer) {
			final Execution exec = Executions.getCurrent();
			final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
			final Component[] cs;

			if (resolver != null)
				exec.addVariableResolver(resolver);

			Page prevPage = null;
			Page page = parent != null ? parent.getPage() : null;
			final boolean fakepg = page == null;
			if (fakepg) {
				prevPage = execCtrl.getCurrentPage();
				page = new VolatilePage(prevPage);
				((PageCtrl) page).preInit();
				execCtrl.setCurrentPage(page);
			}
			try {
				cs = execCreate0(
						new CreateInfo(((WebAppCtrl) exec.getDesktop().getWebApp()).getUiFactory(), exec, page,
								composer), //technically sys composer can be used but we don't (to simplify it)
						_tempInfo, parent, insertBefore);

				//Notice: if parent is not null, cs[j].page == parent.page
				if (fakepg && parent == null)
					for (Component c : cs)
						c.detach();

				afterCreate(exec, null, cs);
			} finally {
				if (fakepg) {
					execCtrl.setCurrentPage(prevPage);
					try {
						((DesktopCtrl) exec.getDesktop()).removePage(page);
					} catch (Throwable ex) {
						log.warn("", ex);
					}
					((PageCtrl) page).destroy();
				}
				if (resolver != null)
					exec.removeVariableResolver(resolver);
			}

			final Component c2 = _src != null ? exec.createComponents(_src, parent, insertBefore, resolver) : null;
			return merge(cs, c2);
		}

		public Map<String, Object> getParameters() {
			return _params;
		}
		public String getSrc() {
			return _src;
		}

		public TemplateInfo getMeta() {
			return _tempInfo;
		}
	}

	/** The listener to create children when the fulfill condition is
	 * satisfied.
	 */
	private static class FulfillListener
			implements EventListener<Event>, java.io.Serializable, Cloneable, ComponentCloneListener {
		private String[] _evtnms;
		private Component[] _targets;
		private Component _comp;
		private final ComponentInfo _compInfo;
		private final String _fulfill;
		private transient String _uri;

		private FulfillListener(String fulfill, ComponentInfo compInfo, Component comp) {
			_fulfill = fulfill;
			_compInfo = compInfo;
			_comp = comp;

			// Fix 5792
			((ComponentCtrl) _comp).addCallback(ComponentCtrl.AFTER_CLONED,
					(Callback) data -> willClone((Component) data));

			init();

			for (int j = _targets.length; --j >= 0;)
				_targets[j].addEventListener(10000, _evtnms[j], this);
		}

		private void init() {
			_uri = null;
			final List<Object[]> results = new LinkedList<Object[]>();
			for (int j = 0, len = _fulfill.length();;) {
				int k = j;
				for (int elcnt = 0; k < len; ++k) {
					final char cc = _fulfill.charAt(k);
					if (elcnt == 0) {
						if (cc == ',')
							break;
						if (cc == '=') {
							_uri = _fulfill.substring(k + 1).trim();
							break;
						}
					} else if (cc == '{') {
						++elcnt;
					} else if (cc == '}') {
						if (elcnt > 0)
							--elcnt;
					}
				}

				String sub = (k >= 0 ? _fulfill.substring(j, k) : _fulfill.substring(j)).trim();
				if (sub.length() > 0)
					results.add(ComponentsCtrl.parseEventExpression(_comp, sub, _comp, false));

				if (_uri != null || k < 0 || (j = k + 1) >= len)
					break;
			}

			int j = results.size();
			_targets = new Component[j];
			_evtnms = new String[j];
			j = 0;
			for (Iterator<Object[]> it = results.iterator(); it.hasNext(); ++j) {
				final Object[] result = it.next();
				_targets[j] = (Component) result[0];
				_evtnms[j] = (String) result[1];
			}
		}

		public void onEvent(Event evt) throws Exception {
			for (int j = _targets.length; --j >= 0;)
				_targets[j].removeEventListener(_evtnms[j], this); //one shot only

			// Fix ZK-5551
			if (_comp.getDesktop() == null && _comp.getPage() == null)
				return; //already detached

			final Execution exec = Executions.getCurrent();
			execCreate0(
					new CreateInfo(((WebAppCtrl) exec.getDesktop().getWebApp()).getUiFactory(), exec, _comp.getPage(),
							null), //technically sys composer can be used but we don't (to simplify it)
					_compInfo, _comp, null);

			if (_uri != null) {
				final String uri = (String) Evaluators.evaluate(_compInfo.getEvaluator(), _comp, _uri, String.class);
				if (uri != null)
					exec.createComponents(uri, _comp, null);
			}

			Events.sendEvent(new FulfillEvent(Events.ON_FULFILL, _comp, evt));
			//Use sendEvent so onFulfill will be processed before
			//the event triggers the fulfill (i.e., evt)
		}

		//ComponentCloneListener//
		public Object willClone(Component comp) {
			final FulfillListener clone;
			try {
				clone = (FulfillListener) clone();
			} catch (CloneNotSupportedException e) {
				throw new InternalError();
			}
			// TODO: the original implementation assumed the comp is the _comp, but this listener is added to _targets, not _comp.
			clone._comp = comp;
			clone.init();
			return clone;
		}
	}

	private static class ReplaceableText {
		private String text;
	}

	//performance meter//
	/** Handles the client complete of AU request for performance measurement.
	 */
	private static void meterAuClientComplete(PerformanceMeter pfmeter, Execution exec) {
		//Format of ZK-Client-Complete and ZK-Client-Receive:
		//	request-id1=time1,request-id2=time2
		String hdr = meterGetData(exec, "ZK-Client-Receive");
		if (hdr != null)
			meterAuClient(pfmeter, exec, hdr, false);
		hdr = meterGetData(exec, "ZK-Client-Complete");
		if (hdr != null)
			meterAuClient(pfmeter, exec, hdr, true);
	}

	private static void meterAuClient(PerformanceMeter pfmeter, Execution exec, String hdr, boolean complete) {
		for (int j = 0;;) {
			int k = hdr.indexOf(',', j);
			String ids = k >= 0 ? hdr.substring(j, k) : j == 0 ? hdr : hdr.substring(j);

			int x = ids.lastIndexOf('=');
			if (x > 0) {
				try {
					long time = Long.parseLong(ids.substring(x + 1));

					ids = ids.substring(0, x);
					for (int y = 0;;) {
						int z = ids.indexOf(' ', y);
						String pfReqId = z >= 0 ? ids.substring(y, z) : y == 0 ? ids : ids.substring(y);
						if (complete)
							pfmeter.requestCompleteAtClient(pfReqId, exec, time);
						else
							pfmeter.requestReceiveAtClient(pfReqId, exec, time);

						if (z < 0)
							break; //done
						y = z + 1;
					}
				} catch (NumberFormatException ex) {
					log.warn("Ingored: unable to parse " + ids);
				} catch (Throwable ex) {
					if (complete)
						log.warn("Ingored: failed to invoke " + pfmeter, ex);
				}
			}

			if (k < 0)
				break; //done
			j = k + 1;
		}
	}

	/** Handles the client and server start of AU request
	 * for the performance measurement.
	 *
	 * @return the request ID from the ZK-Client-Start header,
	 * or null if not found.
	 */
	private static String meterAuStart(PerformanceMeter pfmeter, Execution exec, long startTime) {
		//Format of ZK-Client-Start:
		//	request-id=time
		String hdr = meterGetData(exec, "ZK-Client-Start");
		if (hdr != null) {
			final int j = hdr.lastIndexOf('=');
			if (j > 0) {
				final String pfReqId = hdr.substring(0, j);
				try {
					pfmeter.requestStartAtClient(pfReqId, exec, Long.parseLong(hdr.substring(j + 1)));
					pfmeter.requestStartAtServer(pfReqId, exec, startTime);
				} catch (NumberFormatException ex) {
					log.warn("Ingored: failed to parse ZK-Client-Start, " + hdr);
				} catch (Throwable ex) {
					log.warn("Ingored: failed to invoke " + pfmeter, ex);
				}
				return pfReqId;
			}
		}
		return null;
	}

	/** Handles the server complete of the AU request for the performance measurement.
	 * It sets the ZK-Client-Complete header.
	 *
	 * @param pfReqIds a collection of request IDs that are processed
	 * at the server
	 */
	private static void meterAuServerComplete(PerformanceMeter pfmeter, Collection<String> pfReqIds, Execution exec) {
		final StringBuffer sb = new StringBuffer(256);
		long time = System.currentTimeMillis();
		for (String pfReqId : pfReqIds) {
			if (sb.length() > 0)
				sb.append(' ');
			sb.append(Encode.forUriComponent(pfReqId));

			try {
				pfmeter.requestCompleteAtServer(pfReqId, exec, time);
			} catch (Throwable ex) {
				log.warn("Ingored: failed to invoke " + pfmeter, ex);
			}
		}

		exec.setResponseHeader("ZK-Client-Complete", sb.toString());
		//tell the client what are completed
	}

	/** Handles the (client and) server start of load request
	 * for the performance measurement.
	 *
	 * @return the request ID
	 */
	private static String meterLoadStart(PerformanceMeter pfmeter, Execution exec, long startTime) {
		//Future: handle the zkClientStart parameter
		final String pfReqId = exec.getDesktop().getId();
		try {
			pfmeter.requestStartAtServer(pfReqId, exec, startTime);
		} catch (Throwable ex) {
			log.warn("Ingored: failed to invoke " + pfmeter, ex);
		}
		return pfReqId;
	}

	/** Handles the server complete of the AU request for the performance measurement.
	 * It sets the ZK-Client-Complete header.
	 *
	 * @param pfReqId the request ID that is processed at the server
	 */
	private static void meterLoadServerComplete(PerformanceMeter pfmeter, String pfReqId, Execution exec) {
		try {
			pfmeter.requestCompleteAtServer(pfReqId, exec, System.currentTimeMillis());
		} catch (Throwable ex) {
			log.warn("Ingored: failed to invoke " + pfmeter, ex);
		}
	}

	private static String meterGetData(Execution exec, String key) {
		String param = exec.getParameter(key); // ZK-4204: get pf data by parameters
		return param != null ? param : exec.getHeader(key);
	}

	/** An interface used to extend the UI engine.
	 * The class name of the extension shall be specified in
	 * the library properties called org.zkoss.zk.ui.impl.UiEngineImpl.extension.
	 * <p>Notice that it is used only internally.
	 * @since 5.0.8
	 */
	public static interface Extension {
		/** Called after the whole component tree has been created by
		 * this engine.
		 * <p>The implementation might implement this method to process
		 * the components, such as merging, if necessary.
		 * @param comps the components being created. It is never null but
		 * it might be a zero-length array.
		 */
		public void afterCreate(Component[] comps);

		/** Called after a new page has been redrawn ({@link PageCtrl#redraw}
		 * has been called).
		 * <p>Notice that it is called in the rendering phase (the last phase),
		 * so it is not allowed to post events or to invoke invalidate or smartUpdate
		 * in this method.
		 * <p>Notice that it is not called if an old page is redrawn.
		 * <p>The implementation shall process the components such as merging
		 * if necessary.
		 * @see #execNewPage
		 */
		public void afterRenderNewPage(Page page);

		/** Called when this engine renders the given components.
		 * It is designed to be overridden if you'd like to alter the component
		 * and its children after they are rendered.
		 * @param comps the collection of components that have been redrawn.
		 * @since 6.0.0
		 */
		public void afterRenderComponents(Collection<Component> comps);
	}

	private static class DefaultExtension implements Extension {
		public void afterCreate(Component[] comps) {
		}

		public void afterRenderNewPage(Page page) {
		}

		public void afterRenderComponents(Collection<Component> comps) {
		}
	}
}
