zk/src/main/java/org/zkoss/zk/ui/impl/UiEngineImpl.java

Summary

Maintainability
F
1 mo
Test Coverage
/* 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.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.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.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.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();
        return evt != null && !uv.isAborting() ? evt : 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) {
            if (ex instanceof org.xml.sax.SAXException
                    || ex instanceof org.zkoss.zk.ui.metainfo.PropertyNotFoundException)
                log.error(Exceptions.getMessage(ex));
            else
                log.error("", ex); //Briefly(ex);
        } else {
            ex = t;
            if (log.isDebugEnabled())
                log.debug("", 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", 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");
                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())
            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;

            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

            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();
            }
            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(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) {
        }
    }
}