zk/src/main/java/org/zkoss/zk/ui/util/Configuration.java

Summary

Maintainability
F
2 wks
Test Coverage
/* Configuration.java

    Purpose:
        
    Description:
        
    History:
        Sun Mar 26 16:06:56     2006, Created by tomyeh

Copyright (C) 2006 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.util;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.lang.Classes;
import org.zkoss.lang.Objects;
import org.zkoss.lang.PotentialDeadLockException;
import org.zkoss.util.FastReadArray;
import org.zkoss.util.WaitLock;
import org.zkoss.web.theme.ThemeRegistry;
import org.zkoss.web.theme.ThemeResolver;
import org.zkoss.xel.ExpressionFactory;
import org.zkoss.xel.Expressions;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.au.AuDecoder;
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.RichletConfig;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.ShadowElement;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventThreadCleanup;
import org.zkoss.zk.ui.event.EventThreadInit;
import org.zkoss.zk.ui.event.EventThreadResume;
import org.zkoss.zk.ui.event.EventThreadSuspend;
import org.zkoss.zk.ui.impl.EventInterceptors;
import org.zkoss.zk.ui.impl.MultiComposer;
import org.zkoss.zk.ui.impl.RichletConfigImpl;
import org.zkoss.zk.ui.metainfo.NamespaceParser;
import org.zkoss.zk.ui.sys.DesktopCacheProvider;
import org.zkoss.zk.ui.sys.DiskFileItemFactory;
import org.zkoss.zk.ui.sys.FailoverManager;
import org.zkoss.zk.ui.sys.IdGenerator;
import org.zkoss.zk.ui.sys.PropertiesRenderer;
import org.zkoss.zk.ui.sys.SEORenderer;
import org.zkoss.zk.ui.sys.SessionCache;
import org.zkoss.zk.ui.sys.UiEngine;
import org.zkoss.zk.ui.sys.UiFactory;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.sys.WebAppFactory;

/**
 * The ZK configuration.
 *
 * <p>To retrieve the current configuration, use
 * {@link org.zkoss.zk.ui.WebApp#getConfiguration}.
 *
 * <p>Note: A {@link Configuration} instance can be assigned to at most one
 * {@link WebApp} instance.
 *
 * @author tomyeh
 */
public class Configuration {
    private static final Logger log = LoggerFactory.getLogger(Configuration.class);

    private static final String PROP_EXPRESS_FACTORY = "org.zkoss.xel.ExpressionFactory.class";

    private WebApp _wapp;
    /** List of classes. */
    private final FastReadArray<Class<?>> _evtInits = new FastReadArray<Class<?>>(Class.class),
            _evtCleans = new FastReadArray<Class<?>>(Class.class), _evtSusps = new FastReadArray<Class<?>>(Class.class),
            _evtResus = new FastReadArray<Class<?>>(Class.class), _appInits = new FastReadArray<Class<?>>(Class.class),
            _appCleans = new FastReadArray<Class<?>>(Class.class),
            _sessInits = new FastReadArray<Class<?>>(Class.class),
            _sessCleans = new FastReadArray<Class<?>>(Class.class), _dtInits = new FastReadArray<Class<?>>(Class.class),
            _dtCleans = new FastReadArray<Class<?>>(Class.class), _execInits = new FastReadArray<Class<?>>(Class.class),
            _execCleans = new FastReadArray<Class<?>>(Class.class),
            _composers = new FastReadArray<Class<?>>(Class.class),
            _initiators = new FastReadArray<Class<?>>(Class.class),
            _seoRends = new FastReadArray<Class<?>>(Class.class), _resolvers = new FastReadArray<Class<?>>(Class.class),
            _parsers = new FastReadArray<Class<?>>(Class.class);
    //since it is called frequently, we use array to avoid synchronization
    /** List of objects. */
    private final FastReadArray<AggregationListener> _aggregations = new FastReadArray<AggregationListener>(
            AggregationListener.class); // since 8.0
    private final FastReadArray<URIInterceptor> _uriIntcps = new FastReadArray<URIInterceptor>(URIInterceptor.class);
    private final FastReadArray<RequestInterceptor> _reqIntcps = new FastReadArray<RequestInterceptor>(
            RequestInterceptor.class);
    private final FastReadArray<UiLifeCycle> _uiCycles = new FastReadArray<UiLifeCycle>(UiLifeCycle.class);
    private final FastReadArray<PropertiesRenderer> _propRends = new FastReadArray<PropertiesRenderer>(
            PropertiesRenderer.class);
    private final FastReadArray<String> _labellocs = new FastReadArray<String>(String.class);
    private final Map<String, String> _prefs = Collections.synchronizedMap(new HashMap<String, String>());
    /** Map(String name, [Class richlet, Map params] or Richilet richlet). */
    private final Map<String, Object> _richlets = new HashMap<String, Object>();
    /** Map(String path, [String name, boolean wildcard]). */
    private final Map<String, Object[]> _richletmaps = new HashMap<String, Object[]>();
    /** Map(String deviceType, List(ErrorPage)). */
    private final Map<String, List<ErrorPage>> _errpgs = new HashMap<String, List<ErrorPage>>(4);
    /** Map(String deviceType+connType, Map(errorCode, uri)) */
    private final Map<String, Map<Integer, String>> _errURIs = new HashMap<String, Map<Integer, String>>();
    /** Map(String deviceType, TimeoutURIInfo ti) */
    private final Map<String, TimeoutURIInfo> _timeoutURIs = Collections
            .synchronizedMap(new HashMap<String, TimeoutURIInfo>());
    /** Since 8.0.0 Map(String DataHandlerInfo) */
    private final Map<String, DataHandlerInfo> _dataHandlers = new HashMap<String, DataHandlerInfo>();
    /** Since 8.5.2 Map(String Callback) */
    private final Map<String, Callback> _callbacks = new HashMap<String, Callback>();

    private Monitor _monitor;
    private PerformanceMeter _pfmeter;
    private ExecutionMonitor _execmon;
    private DesktopRecycle _dtRecycle;
    private final FastReadArray<String> _themeURIs = new FastReadArray<String>(String.class);
    private ThemeProvider _themeProvider;
    private List<ThemeURIHandler> _themeURIHandlers = new LinkedList<ThemeURIHandler>();
    /** A set of disabled theme URIs. */
    private Set<String> _disThemeURIs;
    /** A list of client packages. */
    private final FastReadArray<String> _clientpkgs = new FastReadArray<String>(String.class);
    private Class<?> _wappcls, _wappftycls, _uiengcls, _dcpcls, _uiftycls, _failmancls, _idgencls, _sesscachecls,
            _audeccls, _fileFactory;
    private int _dtTimeout = 3600, _sessDktMax = 15, _sessReqMax = 5, _sessPushMax = -1, _sessTimeout = 0,
            _sparThdMax = 100, _suspThdMax = -1, _maxUploadSize = 5120, _fileSizeThreshold, _maxProcTime = 3000,
            _promptDelay = 900, _tooltipDelay = 800, _autoResendTimeout = 200;
    /** since 8.0.2 */
    private String _fileRepository;
    private String _charsetResp = "UTF-8", _charsetUpload = "UTF-8";
    private CharsetFinder _charsetFinderUpload;
    /** The event interceptors. */
    private final EventInterceptors _eis = new EventInterceptors();
    private int _evtTimeWarn = 600; //sec
    /** A map of attributes. */
    private final Map<String, Object> _attrs = Collections.synchronizedMap(new HashMap<String, Object>());
    /** whether to use the event processing thread. */
    private boolean _evtThdEnabled; //disabled by default since ZK 5
    /** whether zscript is enabled. */
    private boolean _zscriptEnabled = true;
    /** keep-across-visits. */
    private boolean _keepDesktop;
    /** Whether to keep the session alive when receiving onTimer.
     */
    private boolean _timerKeepAlive;
    /** Whether to debug JavaScript. */
    private boolean _debugJS;
    /** Whether the ZK application is crawlable. */
    private boolean _crawlable;
    /** Whether to use the same UUID sequence. */
    private boolean _repeatUuid;
    // ZK-1671: ThemeProvider defined in metainfo/zk/zk.xml from jar file doesn't work
    //        Depends on the jar file loading order, user-defined theme provider may be 
    //        overridden by StandardThemeProvider
    private boolean _customThemeProvider = false;
    private boolean _customThemeRegistry = false;
    private boolean _customThemeResolver = false;

    //F70-ZK-2495: init crash script and timeout
    private String _initCrashScript;
    private int _initCrashTimeout = -1;

    //F80 - store subtree's binder annotation count
    private String _binderInitAttribute = null;
    private Set<String> _binderAnnotations;

    private boolean _sourceMapEnabled = false;
    private boolean _historyStateEnabled = true;
    private boolean _sendClientErrors = false;

    /** Constructor.
     */
    public Configuration() {
    }

    /** Returns the Web application that this configuration belongs to,
     * or null if it is not associated yet.
     */
    public WebApp getWebApp() {
        return _wapp;
    }

    /** Associates it with a web application.
     */
    public void setWebApp(WebApp wapp) {
        _wapp = wapp;
    }

    /** Adds a listener class.
     *
     * <p>Notice that there is only one listener allowed for the following classes:
     * {@link Monitor}, {@link PerformanceMeter}, and {@link DesktopRecycle}.
     * On the other hand, any number listeners are allowed for other classes.
     *
     * <p>Notice that if the listener implements {@link Composer}, it can also
     * implement {@link org.zkoss.zk.ui.util.ComposerExt} and/or {@link org.zkoss.zk.ui.util.FullComposer} to have
     * more detailed control. However, ComposerExt and FullComposer are meaningless
     * to richlets. In additions, an independent
     * composer is instantiated for each page so there is synchronization required.
     *
     * <p>By default, a listener is instantiated when required, and dropped
     * after invoked. In other words, a new instance will be instantiated in
     * the next invocation. It means you don't have to worry the threading,
     * <p>However, for better performance, the following listeners will be instantiated
     * in {@link #addListener}, and then used repeatedly. It means it has
     * to be thread safe. These listeners include
     * {@link URIInterceptor}, {@link RequestInterceptor},
     * {@link EventInterceptor}, {@link UiLifeCycle},
     * and {@link PropertiesRenderer}.
     *
     * @param klass the listener class must implement at least one of
     * {@link Monitor}, {@link PerformanceMeter}, {@link EventThreadInit},
     * {@link EventThreadCleanup}, {@link EventThreadSuspend},
     * {@link EventThreadResume}, {@link WebAppInit}, {@link WebAppCleanup},
     * {@link SessionInit}, {@link SessionCleanup}, {@link DesktopInit},
     * {@link DesktopCleanup}, {@link ExecutionInit}, {@link ExecutionCleanup},
     * {@link Composer}, {@link Initiator} (since 5.0.7), {@link SEORenderer} (since 5.0.7),
     * {@link PropertiesRenderer} (since 5.0.7),
     * {@link VariableResolver},
     * {@link URIInterceptor}, {@link RequestInterceptor},
     * {@link UiLifeCycle}, {@link DesktopRecycle},
     * and/or {@link EventInterceptor} interfaces.
     * @see Desktop#addListener
     */
    public void addListener(Class<?> klass) throws Exception {
        boolean added = false;
        Object listener = null;

        if (Monitor.class.isAssignableFrom(klass)) {
            if (_monitor != null)
                throw new UiException(onlyOnce(Monitor.class));
            _monitor = (Monitor) (listener = getInstance(klass, listener));
            added = true;
        }
        if (PerformanceMeter.class.isAssignableFrom(klass)) {
            if (_pfmeter != null)
                throw new UiException(onlyOnce(PerformanceMeter.class));
            _pfmeter = (PerformanceMeter) (listener = getInstance(klass, listener));
            added = true;
        }
        if (ExecutionMonitor.class.isAssignableFrom(klass)) {
            if (_execmon != null)
                throw new UiException(onlyOnce(ExecutionMonitor.class));
            _execmon = (ExecutionMonitor) (listener = getInstance(klass, listener));
            added = true;
        }
        if (DesktopRecycle.class.isAssignableFrom(klass)) {
            if (_dtRecycle != null)
                throw new UiException(onlyOnce(DesktopRecycle.class));
            _dtRecycle = (DesktopRecycle) (listener = getInstance(klass, listener));
            added = true;
        }

        if (EventThreadInit.class.isAssignableFrom(klass)) {
            _evtInits.add(klass);
            added = true;
        }
        if (EventThreadCleanup.class.isAssignableFrom(klass)) {
            _evtCleans.add(klass);
            added = true;
        }
        if (EventThreadSuspend.class.isAssignableFrom(klass)) {
            _evtSusps.add(klass);
            added = true;
        }
        if (EventThreadResume.class.isAssignableFrom(klass)) {
            _evtResus.add(klass);
            added = true;
        }

        if (WebAppInit.class.isAssignableFrom(klass)) {
            _appInits.add(klass);
            added = true;
        }
        if (WebAppCleanup.class.isAssignableFrom(klass)) {
            _appCleans.add(klass);
            added = true;
        }

        if (SessionInit.class.isAssignableFrom(klass)) {
            _sessInits.add(klass);
            added = true;
        }
        if (SessionCleanup.class.isAssignableFrom(klass)) {
            _sessCleans.add(klass);
            added = true;
        }

        if (DesktopInit.class.isAssignableFrom(klass)) {
            _dtInits.add(klass);
            added = true;
        }
        if (DesktopCleanup.class.isAssignableFrom(klass)) {
            _dtCleans.add(klass);
            added = true;
        }

        if (ExecutionInit.class.isAssignableFrom(klass)) {
            _execInits.add(klass);
            added = true;
        }
        if (ExecutionCleanup.class.isAssignableFrom(klass)) {
            _execCleans.add(klass);
            added = true;
        }
        if (Composer.class.isAssignableFrom(klass)) {
            _composers.add(klass); //not instance
            added = true;
        }
        if (Initiator.class.isAssignableFrom(klass)) {
            _initiators.add(klass); //not instance
            added = true;
        }
        if (SEORenderer.class.isAssignableFrom(klass)) {
            _seoRends.add(klass);
            added = true;
        }
        if (VariableResolver.class.isAssignableFrom(klass)) {
            _resolvers.add(klass); //not instance
            added = true;
        }

        //for better performance, the following listeners are instantiated
        //here and shared in the whole application

        if (AggregationListener.class.isAssignableFrom(klass)) {
            try {
                _aggregations.add((AggregationListener) (listener = getInstance(klass, listener)));
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + klass, ex);
            }
            added = true;
        }

        if (URIInterceptor.class.isAssignableFrom(klass)) {
            try {
                _uriIntcps.add((URIInterceptor) (listener = getInstance(klass, listener)));
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + klass, ex);
            }
            added = true;
        }
        if (RequestInterceptor.class.isAssignableFrom(klass)) {
            try {
                _reqIntcps.add((RequestInterceptor) (listener = getInstance(klass, listener)));
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + klass, ex);
            }
            added = true;
        }
        if (EventInterceptor.class.isAssignableFrom(klass)) {
            try {
                _eis.addEventInterceptor((EventInterceptor) (listener = getInstance(klass, listener)));
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + klass, ex);
            }
            added = true;
        }
        if (UiLifeCycle.class.isAssignableFrom(klass)) {
            try {
                _uiCycles.add((UiLifeCycle) (listener = getInstance(klass, listener)));
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + klass, ex);
            }
            added = true;
        }
        if (PropertiesRenderer.class.isAssignableFrom(klass)) {
            try {
                _propRends.add((PropertiesRenderer) (listener = getInstance(klass, listener)));
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + klass, ex);
            }
            added = true;
        }
        if (NamespaceParser.class.isAssignableFrom(klass)) {
            _parsers.add(klass);
            added = true;
        }

        if (!added) {
            for (AggregationListener al : (List<AggregationListener>) Arrays.asList(_aggregations.toArray())) {
                if (al.isHandled(klass)) {
                    added = true;
                    break;
                }
            }
        }

        if (!added)
            throw new UiException("Unknown listener: " + klass);
    }

    private static final String onlyOnce(Class<?> cls) {
        return cls + " can be assigned only once";
    }

    private static Object getInstance(Class<?> klass, Object listener) throws Exception {
        return listener != null ? listener : klass.newInstance();
    }

    /** Removes a listener class.
     * @see Desktop#removeListener
     */
    public void removeListener(final Class<?> klass) {
        if (_monitor != null && _monitor.getClass().equals(klass))
            _monitor = null;
        if (_pfmeter != null && _pfmeter.getClass().equals(klass))
            _pfmeter = null;
        if (_execmon != null && _execmon.getClass().equals(klass))
            _execmon = null;
        if (_dtRecycle != null && _dtRecycle.getClass().equals(klass))
            _dtRecycle = null;

        _evtInits.remove(klass);
        _evtCleans.remove(klass);
        _evtSusps.remove(klass);
        _evtResus.remove(klass);

        _appInits.remove(klass);
        _appCleans.remove(klass);

        _sessInits.remove(klass);
        _sessCleans.remove(klass);

        _dtInits.remove(klass);
        _dtCleans.remove(klass);

        _execInits.remove(klass);
        _execCleans.remove(klass);

        _composers.remove(klass);
        _initiators.remove(klass);
        _seoRends.remove(klass);
        _resolvers.remove(klass);
        _parsers.remove(klass);

        final SameClass sc = new SameClass(klass);

        _aggregations.removeBy(sc, true);
        _uriIntcps.removeBy(sc, true);
        _reqIntcps.removeBy(sc, true);
        _uiCycles.removeBy(sc, true);
        _propRends.removeBy(sc, true);

        _eis.removeEventInterceptor(klass);
    }

    /** Constructs a list of {@link EventThreadInit} instances and invokes
     * {@link EventThreadInit#prepare} for
     * each relevant listener registered by {@link #addListener}.
     *
     * <p>Used only internally (by {@link UiEngine} before starting an event
     * processing thread).
     *
     * @exception UiException to prevent a thread from being processed
     * if {@link EventThreadInit#prepare} throws an exception
     * @return a list of {@link EventThreadInit} instances that are
     * constructed in this method (and their {@link EventThreadInit#prepare}
     * are called successfully), or null.
     */
    public List<EventThreadInit> newEventThreadInits(Component comp, Event evt) throws UiException {
        final Class<?>[] ary = _evtInits.toArray();
        if (ary.length == 0)
            return null;

        final List<EventThreadInit> inits = new LinkedList<EventThreadInit>();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                final EventThreadInit init = (EventThreadInit) klass.newInstance();
                init.prepare(comp, evt);
                inits.add(init);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the event being processed
            }
        }
        return inits;
    }

    /** Invokes {@link EventThreadInit#init} for each instance returned
     * by {@link #newEventThreadInits}.
     *
     * <p>Used only internally.
     *
     * @param inits a list of {@link EventThreadInit} instances returned from
     * {@link #newEventThreadInits}, or null if no instance at all.
     * @param comp the component which the event is targeting
     * @param evt the event to process
     * @exception UiException to prevent a thread from being processed
     * if {@link EventThreadInit#init} throws an exception
     * @return false if you want to ignore the event, i.e., not to proceed
     * any event processing for the specified event (evt).
     */
    public boolean invokeEventThreadInits(List<EventThreadInit> inits, Component comp, Event evt) throws UiException {
        if (inits == null || inits.isEmpty())
            return true; //not to ignore

        for (EventThreadInit fn : inits) {
            try {
                if (!fn.init(comp, evt))
                    return false; //ignore the event
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the event being processed
            }
        }
        return true;
    }

    /** Invokes {@link EventThreadCleanup#cleanup} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link EventThreadCleanup} is constructed first,
     * and then invoke {@link EventThreadCleanup#cleanup}.
     *
     * <p>It never throws an exception but logs and adds it to the errs argument,
     * if not null.
     *
     * @param comp the component which the event is targeting
     * @param evt the event to process
     * @param errs a list of exceptions (java.lang.Throwable) if any exception
     * occurred before this method is called, or null if no exception at all.
     * Note: you can manipulate the list directly to add or clean up exceptions.
     * For example, if exceptions are fixed correctly, you can call errs.clear()
     * such that no error message will be displayed at the client.
     * @param silent whether not to log the exception
     * @return a list of {@link EventThreadCleanup}, or null
     * @since 3.6.3
     */
    public List<EventThreadCleanup> newEventThreadCleanups(Component comp, Event evt, List<Throwable> errs,
            boolean silent) {
        final Class<?>[] ary = _evtCleans.toArray();
        if (ary.length == 0)
            return null;

        final List<EventThreadCleanup> cleanups = new LinkedList<EventThreadCleanup>();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                final EventThreadCleanup cleanup = (EventThreadCleanup) klass.newInstance();
                cleanup.cleanup(comp, evt, errs);
                cleanups.add(cleanup);
            } catch (Throwable t) {
                if (errs != null)
                    errs.add(t);
                if (!silent)
                    log.error("Failed to invoke " + klass, t);
            }
        }
        return cleanups.isEmpty() ? null : cleanups;
    }

    /** Invoke {@link EventThreadCleanup#complete} for each instance returned by
     * {@link #newEventThreadCleanups}.
     *
     * <p>Used only internally.
     *
     * <p>It never throws an exception but logs and adds it to the errs argument,
     * if not null.
     *
     * @param cleanups a list of {@link EventThreadCleanup} instances returned from
     * {@link #newEventThreadCleanups}, or null if no instance at all.
     * @param errs used to hold the exceptions that are thrown by
     * {@link EventThreadCleanup#complete}.
     * If null, all exceptions are ignored (but logged).
     * @param silent whether not to log the exception
     * @since 3.6.3
     */
    public void invokeEventThreadCompletes(List<EventThreadCleanup> cleanups, Component comp, Event evt,
            List<Throwable> errs, boolean silent) {
        if (cleanups == null || cleanups.isEmpty())
            return;

        for (EventThreadCleanup fn : cleanups) {
            try {
                fn.complete(comp, evt);
            } catch (Throwable ex) {
                if (errs != null)
                    errs.add(ex);
                if (!silent)
                    log.error("Failed to invoke " + fn, ex);
            }
        }
    }

    /** Constructs a list of {@link EventThreadSuspend} instances and invokes
     * {@link EventThreadSuspend#beforeSuspend} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>Note: caller shall execute in the event processing thread.
     *
     * @param comp the component which the event is targeting
     * @param evt the event to process
     * @param obj which object that {@link Executions#wait}
     * is called with.
     * @exception UiException to prevent a thread from suspending
     * @return a list of {@link EventThreadSuspend}, or null
     */
    public List<EventThreadSuspend> newEventThreadSuspends(Component comp, Event evt, Object obj) {
        final Class<?>[] ary = _evtSusps.toArray();
        if (ary.length == 0)
            return null;

        final List<EventThreadSuspend> suspends = new LinkedList<EventThreadSuspend>();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                final EventThreadSuspend suspend = (EventThreadSuspend) klass.newInstance();
                suspend.beforeSuspend(comp, evt, obj);
                suspends.add(suspend);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the event being suspended
            }
        }
        return suspends;
    }

    /** Invokes {@link EventThreadSuspend#afterSuspend} for each relevant
     * listener registered by {@link #addListener}.
     * Unlike {@link #invokeEventThreadSuspends}, caller shall execute in
     * the main thread (a.k.a., servlet thread).
     *
     * <p>Used only internally.
     *
     * <p>Unlike {@link #invokeEventThreadSuspends}, exceptions are logged
     * and ignored.
     *
     * @param suspends a list of {@link EventThreadSuspend} instances returned
     * from {@link #newEventThreadSuspends}, or null if no instance at all.
     * @param comp the component which the event is targeting
     * @param evt the event to process
     */
    public void invokeEventThreadSuspends(List<EventThreadSuspend> suspends, Component comp, Event evt)
            throws UiException {
        if (suspends == null || suspends.isEmpty())
            return;

        for (EventThreadSuspend fn : suspends) {
            try {
                fn.afterSuspend(comp, evt);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + fn + " after suspended", ex);
            }
        }
    }

    /** Constructs a list of {@link EventThreadResume} instances and invokes
     * {@link EventThreadResume#beforeResume} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally (by {@link UiEngine} when resuming a suspended event
     * thread).
     * Notice: it executes in the main thread (i.e., the servlet thread).
     *
     * @param comp the component which the event is targeting
     * @param evt the event to process
     * @exception UiException to prevent a thread from being resumed
     * if {@link EventThreadResume#beforeResume} throws an exception
     * @return a list of {@link EventThreadResume} instances that are constructed
     * in this method (and their {@link EventThreadResume#beforeResume}
     * are called successfully), or null.
     */
    public List<EventThreadResume> newEventThreadResumes(Component comp, Event evt) throws UiException {
        final Class<?>[] ary = _evtResus.toArray();
        if (ary.length == 0)
            return null;

        final List<EventThreadResume> resumes = new LinkedList<EventThreadResume>();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                final EventThreadResume resume = (EventThreadResume) klass.newInstance();
                resume.beforeResume(comp, evt);
                resumes.add(resume);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the event being resumed
            }
        }
        return resumes;
    }

    /** Invokes {@link EventThreadResume#afterResume} for each instance returned
     * by {@link #newEventThreadResumes}.
     *
     * <p>Used only internally.
     *
     * <p>It never throws an exception but logs and adds it to the errs argument,
     * if not null.
     *
     * @param resumes a list of {@link EventThreadResume} instances returned from
     * {@link #newEventThreadResumes}, or null if no instance at all.
     * @param comp the component which the event is targeting
     * @param evt the event to process
     * If null, all exceptions are ignored (but logged)
     */
    public void invokeEventThreadResumes(List<EventThreadResume> resumes, Component comp, Event evt)
            throws UiException {
        if (resumes == null || resumes.isEmpty())
            return;

        for (EventThreadResume fn : resumes) {
            try {
                fn.afterResume(comp, evt);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
            }
        }
    }

    /** Invokes {@link EventThreadResume#abortResume} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link EventThreadResume} is constructed first,
     * and then invoke {@link EventThreadResume#abortResume}.
     *
     * <p>It never throws an exception but logging.
     *
     * @param comp the component which the event is targeting
     * @param evt the event to process
     */
    public void invokeEventThreadResumeAborts(Component comp, Event evt) {
        final Class<?>[] ary = _evtResus.toArray();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                ((EventThreadResume) klass.newInstance()).abortResume(comp, evt);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + klass + " for aborting", ex);
            }
        }
    }

    /** Invokes {@link WebAppInit#init} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link WebAppInit} is constructed first,
     * and then invoke {@link WebAppInit#init}.
     *
     * @exception UiException to prevent a webapp from being created
     */
    public void invokeWebAppInits() throws UiException {
        final Class<?>[] ary = _appInits.toArray();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                ((WebAppInit) klass.newInstance()).init(_wapp);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the creation of a webapp
            }
        }
    }

    /** Invokes {@link WebAppCleanup#cleanup} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link WebAppCleanup} is constructed first,
     * and then invoke {@link WebAppCleanup#cleanup}.
     *
     * <p>It never throws an exception.
     */
    public void invokeWebAppCleanups() {
        final Class<?>[] ary = _appCleans.toArray();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                ((WebAppCleanup) klass.newInstance()).cleanup(_wapp);
            } catch (NoClassDefFoundError ex) { //Bug 3046360
            } catch (Throwable ex) {
                log.error("Ignored: failed to invoke " + klass, ex);
            }
        }
    }

    /** Invokes {@link SessionInit#init} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link SessionInit} is constructed first,
     * and then invoke {@link SessionInit#init}.
     *
     * @param sess the session that is created
     * @param request the original request. If HTTP, it is
     * javax.servlet.http.HttlServletRequest.
     * @exception UiException to prevent a session from being created
     * @since 3.0.1
     */
    public void invokeSessionInits(Session sess, Object request) throws UiException {
        final Class<?>[] ary = _sessInits.toArray();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                final SessionInit fn = (SessionInit) klass.newInstance();
                fn.init(sess, request);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the creation of a session
            }
        }
    }

    /** Invokes {@link SessionCleanup#cleanup} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link SessionCleanup} is constructed first,
     * and then invoke {@link SessionCleanup#cleanup}.
     *
     * <p>It never throws an exception.
     *
     * @param sess the session that is being destroyed
     */
    public void invokeSessionCleanups(Session sess) {
        final Class<?>[] ary = _sessCleans.toArray();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                ((SessionCleanup) klass.newInstance()).cleanup(sess);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + klass, ex);
            }
        }
    }

    /** Invokes {@link DesktopInit#init} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link DesktopInit} is constructed first,
     * and then invoke {@link DesktopInit#init}.
     *
     * @param desktop the desktop that is created
     * @param request the original request. If HTTP, it is
     * javax.servlet.http.HttlServletRequest.
     * @exception UiException to prevent a desktop from being created
     * @since 3.0.1
     */
    public void invokeDesktopInits(Desktop desktop, Object request) throws UiException {
        final Class<?>[] ary = _dtInits.toArray();
        for (int j = 0; j < ary.length; ++j) {
            final Class<?> klass = ary[j];
            try {
                final DesktopInit fn = (DesktopInit) klass.newInstance();
                fn.init(desktop, request);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the creation of a session
            }
        }
    }

    /** Invokes {@link DesktopCleanup#cleanup} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link DesktopCleanup} is constructed first,
     * and then invoke {@link DesktopCleanup#cleanup}.
     *
     * <p>It never throws an exception.
     *
     * @param desktop the desktop that is being destroyed
     */
    public void invokeDesktopCleanups(Desktop desktop) {
        final Class<?>[] ary = _dtCleans.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ((DesktopCleanup) ary[j].newInstance()).cleanup(desktop);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes {@link ExecutionInit#init} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link ExecutionInit} is constructed first,
     * and then invoke {@link ExecutionInit#init}.
     *
     * @param exec the execution that is created
     * @param parent the previous execution, or null if no previous at all
     * @exception UiException to prevent an execution from being created
     */
    public void invokeExecutionInits(Execution exec, Execution parent) throws UiException {
        final Class<?>[] ary = _execInits.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ((ExecutionInit) ary[j].newInstance()).init(exec, parent);
            } catch (Throwable ex) {
                throw UiException.Aide.wrap(ex);
                //Don't intercept; to prevent the creation of a session
            }
        }
    }

    /** Invokes {@link ExecutionCleanup#cleanup} for each relevant
     * listener registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>An instance of {@link ExecutionCleanup} is constructed first,
     * and then invoke {@link ExecutionCleanup#cleanup}.
     *
     * <p>It never throws an exception but logs and adds it to the errs argument,
     * if not null.
     *
     * @param exec the execution that is being destroyed
     * @param parent the previous execution, or null if no previous at all
     * @param errs a list of exceptions (java.lang.Throwable) if any exception
     * occurred before this method is called, or null if no exception at all.
     * Note: you can manipulate the list directly to add or clean up exceptions.
     * For example, if exceptions are fixed correctly, you can call errs.clear()
     * such that no error message will be displayed at the client.
     */
    public void invokeExecutionCleanups(Execution exec, Execution parent, List<Throwable> errs) {
        final Class<?>[] ary = _execCleans.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ((ExecutionCleanup) ary[j].newInstance()).cleanup(exec, parent, errs);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
                if (errs != null)
                    errs.add(ex);
            }
        }
    }

    /** Invokes {@link URIInterceptor#request} for each relevant listener
     * registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>If any of them throws an exception, the exception is propagated to
     * the caller.
     *
     * @exception UiException if it is rejected by the interceptor.
     * Use {@link UiException#getCause} to retrieve the cause.
     */
    public void invokeURIInterceptors(String uri) {
        URIInterceptor[] ary = _uriIntcps.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].request(uri);
            } catch (Exception ex) {
                throw UiException.Aide.wrap(ex);
            }
        }
    }

    /** Invokes {@link RequestInterceptor#request} for each relevant listener
     * registered by {@link #addListener}.
     *
     * <p>Used only internally.
     *
     * <p>If any of them throws an exception, the exception is propagated to
     * the caller.
     *
     * @exception UiException if it is rejected by the interceptor.
     * Use {@link UiException#getCause} to retrieve the cause.
     */
    public void invokeRequestInterceptors(Session sess, Object request, Object response) {
        RequestInterceptor[] ary = _reqIntcps.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].request(sess, request, response);
            } catch (Exception ex) {
                throw UiException.Aide.wrap(ex);
            }
        }
    }

    /** Adds the location of a properties file for i18n labels.
     * <p>Default: none (/WEB-INF/zk-label.properties is assumed).
     * <p>Notice that this method has no effect after the web server has been
     * started. Thus, it is suggested to use the label-location element in zk.xml instead.
     * @since 5.0.7
     */
    public void addLabelLocation(String location) {
        if (location == null || location.length() == 0)
            throw new IllegalArgumentException();
        _labellocs.add(location);
    }

    /** Returns an array of the locations of properties files registered
     * by {@link #addLabelLocation}.
     * @since 5.0.7
     */
    public String[] getLabelLocations() {
        return _labellocs.toArray();
    }

    /** Returns the system-level composer or null if none is registered.
     * To register a system-level composer, use {@link #addListener}.
     * <p>Notice that any number of composers can be registered,
     * and a single composer is returned to represent them all.
     * @since 5.0.1
     */
    public Composer<?> getComposer(Page page) throws Exception {
        return MultiComposer.getComposer(page, _composers.toArray());
    }

    /** Returns a readonly list of the system-level initiators.
     * It is empty if none is registered.
     * To register a system-level initiator, use {@link #addListener}.
     * @since 5.0.7
     */
    public Initiator[] getInitiators() {
        final Class<?>[] initclses = _initiators.toArray();
        if (initclses.length == 0)
            return new Initiator[0];

        final List<Initiator> inits = new LinkedList<Initiator>();
        for (int j = 0; j < initclses.length; ++j) {
            try {
                inits.add((Initiator) initclses[j].newInstance());
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + initclses[j]);
            }
        }
        return inits.toArray(new Initiator[inits.size()]);
    }

    /** Returns a readonly list of the system-level NamespaceParsers.
     * It is empty if none is registered.
     * To register a system-level NamespaceParsers, use {@link #addListener}.
     * @since 7.0.3
     */
    @SuppressWarnings("unchecked")
    public List<NamespaceParser> getNamespaceParsers() {
        final Class<?>[] initclses = _parsers.toArray();
        if (initclses.length == 0)
            return Collections.EMPTY_LIST;

        final List<NamespaceParser> inits = new LinkedList<NamespaceParser>();
        for (int j = 0; j < initclses.length; ++j) {
            try {
                inits.add((NamespaceParser) initclses[j].newInstance());
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + initclses[j]);
            }
        }
        return inits;
    }

    /** Returns a readonly list of the system-level SEO renderer.
     * It is empty if none is registered.
     * To register a system-level SEO renderers, use {@link #addListener}.
     * <p>Notice that, once registered, an instance is instantiated before
     * invoking {@link SEORenderer#render}.
     * @since 5.0.7
     */
    public SEORenderer[] getSEORenderers() {
        final Class<?>[] sdclses = _seoRends.toArray();
        if (sdclses.length == 0)
            return new SEORenderer[0];

        final List<SEORenderer> sds = new LinkedList<SEORenderer>();
        for (int j = 0; j < sdclses.length; ++j) {
            try {
                sds.add((SEORenderer) sdclses[j].newInstance());
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + sdclses[j]);
            }
        }
        return sds.toArray(new SEORenderer[sds.size()]);
    }

    /** Initializes the given page with the variable resolvers registered
     * by {@link #addListener}.
     * It must be called before accessing a page (actually in {@link org.zkoss.zk.ui.sys.PageCtrl#preInit}).
     * @since 5.0.4
     */
    public void init(Page page) {
        final Class<?>[] classes = _resolvers.toArray();
        for (int j = 0; j < classes.length; ++j) {
            try {
                page.addVariableResolver((VariableResolver) classes[j].newInstance());
            } catch (Throwable ex) {
                log.error("Failed to instantiate " + classes[j], ex);
            }
        }
    }

    /** Returns a readonly list of the system-level properties renders.
     * It is empty if none is registered.
     * To register a system-level properties renders, use {@link #addListener}.
     * <p>Notice that, once registered, it is instantiated immediately,
     * and the same instance is shared for rendering the properties of every component.
     * @since 5.0.7
     */
    public PropertiesRenderer[] getPropertiesRenderers() {
        return _propRends.toArray();
    }

    /** Invokes {@link UiLifeCycle#afterComponentAttached}
     * when a component is attached to a page.
     * @since 3.0.6
     */
    public void afterComponentAttached(Component comp, Page page) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterComponentAttached(comp, page);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes {@link UiLifeCycle#afterComponentDetached}
     * when a component is detached from a page.
     * @since 3.0.6
     */
    public void afterComponentDetached(Component comp, Page prevpage) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterComponentDetached(comp, prevpage);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes a callback function {@link Callback#call(Object data)}
     * @param name the name of the callback function.
     * @param data the parameter for the callback function.
     * @since 8.5.2
     */
    public void invokeCallback(String name, Object data) {
        Callback callback = _callbacks.get(name);
        if (callback != null)
            callback.call(data);
    }

    /** Register a callback function {@link Callback#call(Object data)}
     * @param name the name of the callback function.
     * @param callback the callback function to register into Configuration class. 
     * @since 8.5.2
     */
    public void registerCallBack(String name, Callback callback) {
        _callbacks.put(name, callback);
    }

    /** Unregister a callback function {@link Callback#call(Object data)}
     * @param name the name of the callback function.
     * @since 8.5.2
     */
    public void unregisterCallBack(String name) {
        _callbacks.remove(name);
    }

    /** Returns if a callback function {@link Callback#call(Object data)} is registered.
     * @param name the name of the callback function.
     * @return If a callback function is registered.
     * @since 8.5.2
     */
    public boolean hasCallBack(String name) {
        return _callbacks.containsKey(name);
    }

    /** Invokes {@link UiLifeCycle#afterShadowAttached(ShadowElement, Component)}
     * when a shadow is attached to a host.
     * @since 8.0.0
     */
    public void afterShadowAttached(ShadowElement shadow, Component host) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterShadowAttached(shadow, host);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes {@link UiLifeCycle#afterShadowDetached(ShadowElement, Component)}
     * when a shadow is detached from a host.
     * @since 8.0.0
     */
    public void afterShadowDetached(ShadowElement shadow, Component prevhost) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterShadowDetached(shadow, prevhost);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes {@link UiLifeCycle#afterComponentMoved}
     * when a component is moved (a.k.a., page changed).
     * @since 3.0.6
     */
    public void afterComponentMoved(Component parent, Component child, Component prevparent) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterComponentMoved(parent, child, prevparent);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes {@link UiLifeCycle#afterPageAttached}
     * when a compnent's parent is changed.
     * @since 3.0.6
     */
    public void afterPageAttached(Page page, Desktop desktop) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterPageAttached(page, desktop);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Invokes {@link UiLifeCycle#afterPageDetached}
     * when a compnent's parent is changed.
     * @since 3.0.6
     */
    public void afterPageDetached(Page page, Desktop prevdesktop) {
        final UiLifeCycle[] ary = _uiCycles.toArray();
        for (int j = 0; j < ary.length; ++j) {
            try {
                ary[j].afterPageDetached(page, prevdesktop);
            } catch (Throwable ex) {
                log.error("Failed to invoke " + ary[j], ex);
            }
        }
    }

    /** Adds an CSS resource that will be generated for each ZUML desktop.
     *
     * <p>Note: if {@link ThemeProvider} is specified ({@link #setThemeProvider}),
     * the final theme URIs generated depend on {@link ThemeProvider#getThemeURIs}.
     */
    public void addThemeURI(String uri) {
        if (uri == null || uri.length() == 0)
            throw new IllegalArgumentException("empty");
        _themeURIs.add(uri);
    }

    /** Returns a readonly list of the URI of the CSS resources that will be
     * generated for each ZUML desktop (never null).
     *
     * <p>Default: an array with zero length.
     */
    public String[] getThemeURIs() {
        return _themeURIs.toArray();
    }

    /** Specifies what theme URI to be disabled.
     *
     * <p>Note: if {@link ThemeProvider} is used ({@link #setThemeProvider}),
     * the URIs of the theme depend on {@link ThemeProvider#getThemeURIs}.
     *
     * @param uri the theme URI to disable
     * @since 3.0.0
     */
    public void addDisabledThemeURI(String uri) {
        if (uri == null || uri.length() == 0)
            throw new IllegalArgumentException();

        synchronized (this) {
            if (_disThemeURIs == null)
                _disThemeURIs = Collections.synchronizedSet(new HashSet<String>(4));
        }
        _disThemeURIs.add(uri);
    }

    /** Returns a set of the theme URIs that are disabled (never null).
     *
     * @since 3.0.0
     * @see #addDisabledThemeURI
     */
    public Set<String> getDisabledThemeURIs() {
        if (_disThemeURIs != null)
            return _disThemeURIs;
        return Collections.emptySet();
    }

    /**
     * Add a {@link ThemeURIHandler}.
     *
     * <p>Note: if {@link ThemeProvider} is specified ({@link #setThemeProvider}),
     * the final generated theme URIs depend on {@link ThemeProvider#getThemeURIs}.
     *
     * @since 9.6.0
     */
    public void addThemeURIHandler(ThemeURIHandler themeURIHandler) {
        if (themeURIHandler == null)
            throw new IllegalArgumentException("empty");
        _themeURIHandlers.add(themeURIHandler);
    }

    /**
     * Returns a list of {@link ThemeURIHandler}.
     *
     * @since 9.6.0
     */
    public List<ThemeURIHandler> getThemeURIHandlers() {
        return _themeURIHandlers;
    }

    /** Returns the theme provider for the current execution,
     * or null if not available.
     *
     * <p>Default: null.
     *
     * <p>Note: if specified, the final theme URIs is decided by
     * the provider. The URIs specified in {@link #getThemeURIs} are
     * passed to provider, and it has no effect if the provider decides
     * to ignore them.
     * @since 3.0.0
     * @see #getThemeURIs
     * @see #getDisabledThemeURIs
     */
    public ThemeProvider getThemeProvider() {
        return _themeProvider;
    }

    /** Sets the theme provider for the current execution,
     * or null if not available.
     *
     * @param provider the theme provide. If null, the default theme URIs
     * will be used.
     * @see #getThemeProvider
     * @since 3.0.0
     */
    public void setThemeProvider(ThemeProvider provider) {
        _themeProvider = provider;
    }

    /** Sets the class used to handle UI loading and updates, or null to
     * use the default.
     * It must implement {@link UiEngine}.
     */
    public void setUiEngineClass(Class<?> cls) {
        if (cls != null && !UiEngine.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("UiEngine not implemented: " + cls);
        _uiengcls = cls;
    }

    /** Returns the class used to handle UI loading and updates,
     * or null if default is used.
     * It must implement {@link UiEngine}.
     */
    public Class<?> getUiEngineClass() {
        return _uiengcls;
    }

    /** Sets the class used to represent this Web application,
     * or null to use the default.
     * It must implement {@link WebApp} and {@link WebAppCtrl}
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     */
    public void setWebAppClass(Class<?> cls) {
        if (cls != null && (!WebApp.class.isAssignableFrom(cls) || !WebAppCtrl.class.isAssignableFrom(cls)))
            throw new IllegalArgumentException("WebApp or WebAppCtrl not implemented: " + cls);
        _wappcls = cls;
    }

    /** Returns the class used to represent this Web application,
     * or null if default is used.
     * It must implement {@link WebApp} and {@link WebAppCtrl}
     */
    public Class<?> getWebAppClass() {
        return _wappcls;
    }

    /** Sets the class used to instantiate an instance representing this Web application,
     * or null to use the default.
     *
     * <p>Note: {@link #setWebAppClass} has the higher priority if not null.
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     * @param cls the class that implements {@link WebAppFactory}.
     * @since 6.0.0
     */
    public void setWebAppFactoryClass(Class<?> cls) {
        if (cls != null && !WebAppFactory.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("WebAppFactory not implemented: " + cls);
        _wappftycls = cls;
    }

    /** Returns the class used to instantiate an instance representing this Web application,
     * or null if default is used.
     * It must implement {@link WebAppFactory}.
     * <p>Note: {@link #getWebAppClass} has the higher priority if not null.
     * @since 6.0.0
     */
    public Class<?> getWebAppFactoryClass() {
        return _wappftycls;
    }

    /** Sets the class used to provide the desktop cache, or null to
     * use the default.
     * It must implement {@link DesktopCacheProvider}.
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     */
    public void setDesktopCacheProviderClass(Class<?> cls) {
        if (cls != null && !DesktopCacheProvider.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("DesktopCacheProvider not implemented: " + cls);
        _dcpcls = cls;
    }

    /** Returns the class used to provide the desktop cache, or null
     * if default is used.
     * It must implement {@link DesktopCacheProvider}.
     */
    public Class<?> getDesktopCacheProviderClass() {
        return _dcpcls;
    }

    /** Sets the class used to instantiate desktops, pages and components, or
     * null to use the default.
     * It must implement {@link UiFactory},
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     */
    public void setUiFactoryClass(Class<?> cls) {
        if (cls != null && !UiFactory.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("UiFactory not implemented: " + cls);
        _uiftycls = cls;
    }

    /** Returns the class used to instantiate desktops, pages and components,
     * or null if default is used.
     * It must implement {@link UiFactory},
     */
    public Class<?> getUiFactoryClass() {
        return _uiftycls;
    }

    /** Sets the class used to handle the failover mechanism, or null if
     * no custom failover mechanism.
     * It must implement {@link FailoverManager}.
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     */
    public void setFailoverManagerClass(Class<?> cls) {
        if (cls != null && !FailoverManager.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("FailoverManager not implemented: " + cls);
        _failmancls = cls;
    }

    /** Returns the class used to handle the failover mechanism,
     * or null if no custom failover mechanism.
     * It must implement {@link FailoverManager}.
     */
    public Class<?> getFailoverManagerClass() {
        return _failmancls;
    }

    /** Sets the class that is used to generate UUID/ID of desktop,
     * page and components, or null to use the default.
     * It must implement {@link IdGenerator}.
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     * @since 2.4.1
     */
    public void setIdGeneratorClass(Class<?> cls) {
        if (cls != null && !IdGenerator.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("IdGenerator not implemented: " + cls);
        _idgencls = cls;
    }

    /** Returns the class used to generate UUID/ID for desktop,
     * page and components, or null if the default shall be used.
     * It must implement {@link IdGenerator}
     * @since 2.4.1
     */
    public Class<?> getIdGeneratorClass() {
        return _idgencls;
    }

    /** Sets the class that is used to store ZK sessions,
     * or null to use the default.
     * It must implement {@link SessionCache}.
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     * @since 3.0.5
     */
    public void setSessionCacheClass(Class<?> cls) {
        if (cls != null && !SessionCache.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("SessionCache not implemented: " + cls);
        _sesscachecls = cls;
    }

    /** Returns the class used to store ZK sessions, or null
     * if the default shall be used.
     * It must implement {@link SessionCache}.
     * @since 3.0.5
     */
    public Class<?> getSessionCacheClass() {
        return _sesscachecls;
    }

    /** Sets the class that is used to create a file item for fileupload,
     * or null to use the default.
     * It must implement {@link DiskFileItemFactory}.
     *
     * @since 8.0.2
     */
    public void setFileItemFactoryClass(Class<?> cls) {
        if (cls != null && !DiskFileItemFactory.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("DiskFileItemFactory not implemented: " + cls);
        _fileFactory = cls;
    }

    /** Returns the class used to create a file item for fileupload, or null
     * if the default shall be used.
     * It must implement {@link DiskFileItemFactory}.
     * @since 8.0.2
     */
    public Class<?> getFileItemFactoryClass() {
        return _fileFactory;
    }

    /** Sets the class that is used to decode AU requests,
     * or null to use the default.
     * It must implement {@link AuDecoder}.
     *
     * <p>Note: you have to set the class before {@link WebApp} is created.
     * Otherwise, it won't have any effect.
     * @since 5.0.4
     */
    public void setAuDecoderClass(Class<?> cls) {
        if (cls != null && !AuDecoder.class.isAssignableFrom(cls))
            throw new IllegalArgumentException("AuDecoder not implemented: " + cls);
        _audeccls = cls;
    }

    /** Returns the class used to decode AU requests, or null
     * if the default shall be used.
     * It must implement {@link AuDecoder}.
     * @since 5.0.4
     */
    public Class<?> getAuDecoderClass() {
        return _audeccls;
    }

    /** Specifies the maximal allowed time to process events, in milliseconds.
     * ZK will keep processing the requests until all requests are processed,
     * or the maximal allowed time expires.
     *
     * <p>Default: 3000.
     *
     * <p>Note: since 3.0.0, this setting has no effect on AU requests.
     * It controls only the requests from the client-polling server push.
     *
     * @param time the maximal allowed time to process events.
     * It must be positive.
     */
    public void setMaxProcessTime(int time) {
        _maxProcTime = time;
    }

    /** Returns the maximal allowed time to process events, in milliseconds.
     * It is always positive.
     */
    public int getMaxProcessTime() {
        return _maxProcTime;
    }

    /** Specifies the maximal allowed upload size, in kilobytes.
     *
     * <p>Default: 5120.
     *
     * @param sz the maximal allowed upload size.
     * A negative value indicates there is no limit.
     */
    public void setMaxUploadSize(int sz) {
        _maxUploadSize = sz;
    }

    /** Returns the maximal allowed upload size, in kilobytes, or 
     * a negative value if no limit.
     */
    public int getMaxUploadSize() {
        return _maxUploadSize;
    }

    /** Specifies the threshold at which a temporary file is created as a 
     * buffer, in kilobytes.
     *
     * <p>Default: 128.
     *
     * @param sz the file size threshold
     *  A negative value implies default setting.
     * @since 5.0.8
     */
    public void setFileSizeThreshold(int sz) {
        _fileSizeThreshold = sz;
    }

    /** Returns the threshold at which a temporary file is created as a 
     * buffer, in kilobytes, or a negative value which implies default setting.
     * @since 5.0.8
     */
    public int getFileSizeThreshold() {
        return _fileSizeThreshold;
    }

    /**
     * Sets the directory in which uploaded files will be stored, if stored on disk.
     * @param directory
     * @since 8.0.2
     */
    public void setFileRepository(String directory) {
        _fileRepository = directory;
    }

    /**
     * Returns the directory in which uploaded files will be stored, if stored on disk.
     * <p>Default: null</p>
     * @since 8.0.2
     */
    public String getFileRepository() {
        return _fileRepository;
    }

    /** Returns the charset used to encode the uploaded text file
     * (never null).
     *
     * <p>Default: UTF-8.
     * @see #getUploadCharsetFinder
     */
    public String getUploadCharset() {
        return _charsetUpload;
    }

    /** Sets the charset used to encode the upload text file.
     *
     * <p>Note: {@link #setUploadCharsetFinder} has the higher priority.
     *
     * @param charset the charset to use.
     * If null or empty, UTF-8 is assumed.
     * @see #setUploadCharsetFinder
     */
    public void setUploadCharset(String charset) {
        _charsetUpload = charset != null && charset.length() > 0 ? charset : "UTF-8";
    }

    /** Returns the finder that is used to decide the character set
     * for the uploaded text file(s), or null if not available.
     *
     * <p>Default: null
     * @since 3.0.0
     * @see #getUploadCharset
     */
    public CharsetFinder getUploadCharsetFinder() {
        return _charsetFinderUpload;
    }

    /** Sets the finder that is used to decide the character set
     * for the uploaded text file(s), or null if not available.
     *
     * <p>It has the higher priority than {@link #setUploadCharset}.
     * In other words, {@link #getUploadCharset} is used only if
     * this method returns null or {@link CharsetFinder#getCharset}
     * returns null.
     *
     * @since 3.0.0
     * @see #setUploadCharset
     */
    public void setUploadCharsetFinder(CharsetFinder finder) {
        _charsetFinderUpload = finder;
    }

    /** Specifies the time, in seconds, between client requests
     * before ZK will invalidate the desktop.
     *
     * <p>Default: 3600 (1 hour).
     *
     * <p>A negative value indicates the desktop should never timeout.
     */
    public void setDesktopMaxInactiveInterval(int secs) {
        _dtTimeout = secs;
    }

    /** Returns the time, in seconds, between client requests
     * before ZK will invalidate the desktop.
     *
     * <p>Notice that this timeout is used only if JVM starts
     * GC when the memory is running low.
     *
     * <p>A negative value indicates the desktop should never timeout.
     */
    public int getDesktopMaxInactiveInterval() {
        return _dtTimeout;
    }

    /** Specifies the time, in milliseconds, before ZK Client Engine shows
     * a dialog to prompt users that the request is in processing.
     *
     * <p>Default: 900
     */
    public void setProcessingPromptDelay(int minisecs) {
        _promptDelay = minisecs;
    }

    /** Returns the time, in milliseconds, before ZK Client Engine shows
     * a dialog to prompt users that the request is in processing.
     */
    public int getProcessingPromptDelay() {
        return _promptDelay;
    }

    /** Specifies the time, in milliseconds, before ZK Client Engine shows
     * the tooltip when a user moves the mouse over particular UI components.
     *
     * <p>Default: 800
     */
    public void setTooltipDelay(int minisecs) {
        _tooltipDelay = minisecs;
    }

    /** Returns the time, in milliseconds, before ZK Client Engine shows
     * the tooltip when a user moves the mouse over particular UI components.
     */
    public int getTooltipDelay() {
        return _tooltipDelay;
    }

    /** Specifies the timeout, in milliseconds, to re-send the AU request when
     * the server's service unavailable or timeout.
     * <p>Default: 200
     * @since 6.5.2
     */
    public void setAutoResendTimeout(int minisecs) {
        _autoResendTimeout = minisecs;
    }

    /** Returns the timeout, in milliseconds, for re-sending the AU request when
     * the server's service unavailable or timeout.
     * @since 6.5.2
     * <p>Default: 200
     */
    public int getAutoResendTimeout() {
        return _autoResendTimeout;
    }

    /** Returns whether this Web application can be crawled by search engines.
     * Notice that there is some performance loss for huge web pages.
     * <p>Default: false.
     * @since 5.0.0
     */
    public boolean isCrawlable() {
        return _crawlable;
    }

    /** Sets whether this Web application is crawlable.
     * Make a Web application that allows search engines to crawl the application.
     * Notice that there is some performance loss for huge web pages. (EE only)
     * @since 5.0.0
     */
    public void setCrawlable(boolean crawlable) {
        if (crawlable && !WebApps.getFeature("ee")) {
            log.warn("The crawlable setting is for EE edtion only!");
            return;
        }
        _crawlable = crawlable;
    }

    /** Returns the timeout URI for this device.
     * It is used to show the error message if the desktop being requested
     * is not found. It is usually caused by session timeout.
     *
     * <p>Default: null (to shown an error message).
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @since 3.6.3
     */
    public URIInfo getTimeoutURI(String deviceType) {
        if (deviceType == null)
            deviceType = "ajax";

        TimeoutURIInfo inf = _timeoutURIs.get(deviceType);
        return inf != null && inf.uri != null ? inf : null;
    }

    /** Sets the timeout URI.
     * It is used to show the error message if the desktop being requested
     * is not found. It is usually caused by session timeout.
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @param timeoutURI the timeout URI. If empty, it means to reload
     * the same page. If null, an error message is shown instead of
     * redirecting to another page.
     * @param type how to handle the timeout URI. It is one of
     * {@link URIInfo#SEND_REDIRECT} or {@link URIInfo#POPUP}.
     * However, it supports only {@link URIInfo#SEND_REDIRECT} currently.
     * @return the previous timeout URI, or null if not available.
     * @since 3.6.3
     */
    public URIInfo setTimeoutURI(String deviceType, String timeoutURI, int type) {
        if (deviceType == null)
            deviceType = "ajax";

        TimeoutURIInfo newi = new TimeoutURIInfo(timeoutURI, type);
        TimeoutURIInfo oldi = _timeoutURIs.put(deviceType, newi);
        if (oldi != null)
            newi.auto = oldi.auto;
        return oldi != null && oldi.uri != null ? oldi : null;
    }

    /** Returns the timeout message for this device, or null if the default
     * message is preferred.
     * It is used only if {@link #getTimeoutURI} returns null.
     * @since 5.0.5
     * @see #setTimeoutMessage
     */
    public String getTimeoutMessage(String deviceType) {
        if (deviceType == null)
            deviceType = "ajax";

        TimeoutURIInfo inf = _timeoutURIs.get(deviceType);
        return inf != null ? inf.message : null;
    }

    /** Sets the timeout message for this device, or null if the default
     * message is preferred.
     * It is used only if {@link #getTimeoutURI} returns null.
     * <p>To specify an I18N label, prefix the key with <code>label:</code>.
     * To specify the JavaScript code, prefix the code with <code>script:</code>.
     * Refer to <a href="http://books.zkoss.org/wiki/ZK_Configuration_Reference/zk.xml/The_session-config_Element#The_timeout-message_Element">ZK Configuration Reference</a>
     * for more information.
     * @return the previous message, if any
     * @since 5.0.5
     */
    public String setTimeoutMessage(String deviceType, String message) {
        if (deviceType == null)
            deviceType = "ajax";

        TimeoutURIInfo inf = _timeoutURIs.get(deviceType);
        if (inf != null) {
            String old = inf.message;
            inf.message = message;
            return old;
        }

        inf = new TimeoutURIInfo();
        inf.message = message;
        _timeoutURIs.put(deviceType, inf);
        return null;
    }

    /** Returns whether to automatically trigger the timeout at the client.
     * Refer to {@link #setAutomaticTimeout} for details.
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @see #setAutomaticTimeout
     * @see #getTimeoutURI
     * @since 3.6.3
     */
    public boolean isAutomaticTimeout(String deviceType) {
        if (deviceType == null)
            deviceType = "ajax";

        TimeoutURIInfo inf = _timeoutURIs.get(deviceType);
        return inf != null && inf.auto;
    }

    /** Sets whether to automatically trigger the timeout at the client.
     *
     * <p>Default: false. It means this page is redirected to the timeout URI
     * when the use takes some action after timeout. In other words,
     * nothing happens if the user does nothing.
     * If it is set to true, it is redirected as soon as the timeout URI,
     * no matter the user takes any action.
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @see #setTimeoutURI
     * @since 3.6.3
     */
    public boolean setAutomaticTimeout(String deviceType, boolean auto) {
        if (deviceType == null)
            deviceType = "ajax";

        TimeoutURIInfo inf = _timeoutURIs.get(deviceType);
        if (inf != null) {
            boolean old = inf.auto;
            inf.auto = auto;
            return old;
        }

        inf = new TimeoutURIInfo();
        inf.auto = auto;
        _timeoutURIs.put(deviceType, inf);
        return false;
    }

    /** Sets the URI to redirect to, when ZK Client Engine receives
     * an error.
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @param errCode the error code.
     * @param uri the URI to redirect to. It cannot be null.
     * If empty, the client will reload the same page again.
     * If null, it is the same as {@link #removeClientErrorReload}
     * @param connType the connection type: au or server-push.
     * If null, "au" is assumed.
     * @return the previous URI associated with the specified error code
     * @since 3.6.3
     */
    public String setClientErrorReload(String deviceType, int errCode, String uri, String connType) {
        if (uri == null)
            return removeClientErrorReload(deviceType, errCode, connType);

        final String index = deviceConn2Str(deviceType, connType);
        synchronized (_errURIs) {
            Map<Integer, String> map = _errURIs.get(index);
            if (map == null)
                _errURIs.put(index, map = new LinkedHashMap<Integer, String>());
            return map.put(errCode, uri);
        }
    }

    /** Removes the URI to redirect to, when ZK Client Engine receives
     * an error.
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @param errCode the error code.
     * @param connType the connection type: au or server-push.
     * If null, "au" is assumed.
     * @return the previous URI associated with the specified error code
     * @since 3.6.3
     */
    public String removeClientErrorReload(String deviceType, int errCode, String connType) {
        final String index = deviceConn2Str(deviceType, connType);
        synchronized (_errURIs) {
            Map<Integer, String> map = _errURIs.get(index);
            return map != null ? map.remove(errCode) : null;
        }
    }

    /** Returns the URI that is associated with the specified error code,
     * or null if no URI is associated.
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @param errCode the error code.
     * @param connType the connection type: au or server-push.
     * If null, "au" is assumed.
     * @since 3.6.3
     */
    public String getClientErrorReload(String deviceType, int errCode, String connType) {
        final String index = deviceConn2Str(deviceType, connType);
        synchronized (_errURIs) {
            Map<Integer, String> map = _errURIs.get(index);
            return map != null ? map.get(errCode) : null;
        }
    }

    /** Returns an array of pairs of the error code and URI info of
     * the specified device and connection (never null).
     *
     * <p>Default: none (none since 3.6.0, while
     * older version: 302, 401 and 403 are associated with an empty URI).
     *
     * @param deviceType the device type: ajax or mil.
     * If null, ajax is assumed.
     * @param connType the connection type: au or server-push.
     * If null, "au" is assumed.
     * @return an array of pairs (two-element arrays) of the error code and URI info.
     * In other words, each element of the returned array is a pair of
     * Integer and {@link URIInfo}.
     * For example, [[410, new URIInfo("/login.zul")], [310, new URIInfo("/login2.zul")]].
     * @since 3.6.3
     */
    public Object[][] getClientErrorReloads(String deviceType, String connType) {
        final String index = deviceConn2Str(deviceType, connType);
        synchronized (_errURIs) {
            final Map<Integer, String> map = _errURIs.get(index);
            if (map != null) {
                Object[][] infs = new Object[map.size()][2];
                int j = 0;
                for (Map.Entry<Integer, String> me : map.entrySet()) {
                    infs[j][0] = me.getKey();
                    infs[j++][1] = new URIInfo(me.getValue());
                }
                return infs;
            }
        }
        return new Object[0][0];
    }

    private static final String deviceConn2Str(String deviceType, String connType) {
        if (deviceType == null)
            deviceType = "ajax";
        return connType != null && "server-push".equals(connType) ? "s:" + deviceType : deviceType;
    }

    /**  Specifies the time, in seconds, between client requests
     * before ZK will invalidate the session.
     *
     * <p>Default: 0 (means the system default).
     *
     * @see #setTimerKeepAlive
     * @see Session#setMaxInactiveInterval
     */
    public void setSessionMaxInactiveInterval(int secs) {
        _sessTimeout = secs;
    }

    /** Returns the time, in seconds, between client requests
     * before ZK will invalidate the session.
     *
     * <p>Default: 0 (means the system default).
     *
     * <p>A negative value indicates that there is no limit.
     * Zero means to use the system default (usually defined in web.xml).
     *
     * @see #isTimerKeepAlive
     * @see Session#getMaxInactiveInterval
     */
    public int getSessionMaxInactiveInterval() {
        return _sessTimeout;
    }

    /** Specifies the maximal allowed number of desktop
     * per session.
     *
     * <p>Default: 15.
     *
     * <p>A negative value indicates there is no limit.
     * @since 3.0.1
     */
    public void setSessionMaxDesktops(int max) {
        _sessDktMax = max;
    }

    /** Returns the maximal allowed number of desktop per session.
     *
     * <p>A negative value indicates there is no limit.
     * @since 3.0.1
     */
    public int getSessionMaxDesktops() {
        return _sessDktMax;
    }

    /** Specifies the maximal allowed number of concurrent requests
     * per session.
     *
     * <p>Default: 5.
     *
     * <p>A negative value indicates there is no limit, but it is
     * not recommended due to the possibility of the DoS attacks.
     * @since 3.0.1
     */
    public void setSessionMaxRequests(int max) {
        _sessReqMax = max;
    }

    /** Returns the maximal allowed number of concurrent requests
     * per session.
     *
     * <p>A negative value indicates there is no limit, but it is
     * not recommended due to the possibility of the DoS attacks.
     * @since 3.0.1
     */
    public int getSessionMaxRequests() {
        return _sessReqMax;
    }

    /** Specifies the maximal allowed number of concurrent server-pushes
     * per session.
     *
     * <p>Default: -1 (no limitation).
     *
     * @param max the maximal allowed number.
     * A negative value indicates there is no limit.
     * @since 5.0.0
     */
    public void setSessionMaxPushes(int max) {
        _sessPushMax = max;
    }

    /** Returns the maximal allowed number of concurrent server-pushes
     * per session.
     */
    public int getSessionMaxPushes() {
        return _sessPushMax;
    }

    /** Specifies the maximal allowed number of the spare pool for
     * queuing the event processing threads (per Web application).
     *
     * <p>Default: 100.
     *
     * <p>A negative value indicates there is no limit.
     *
     * <p>ZK uses a thread pool to keep the idle event processing threads.
     * It speeds up the service of an event by reusing the thread queued
     * in this pool.
     *
     * @see #setMaxSuspendedThreads
     * @see #isEventThreadEnabled
     */
    public void setMaxSpareThreads(int max) {
        _sparThdMax = max;
    }

    /** Returns the maximal allowed number of the spare pool for
     * queuing event processing threads (per Web application).
     * @see #isEventThreadEnabled
     */
    public int getMaxSpareThreads() {
        return _sparThdMax;
    }

    /** Specifies the maximal allowed number of suspended event
     * processing threads (per Web application).
     *
     * <p>Default: -1 (no limit).
     *
     * <p>A negative value indicates there is no limit.
     *
     * <p>It is ignored if the use of the event processing thread
     * is disable ({@link #isEventThreadEnabled}.
     */
    public void setMaxSuspendedThreads(int max) {
        _suspThdMax = max;
    }

    /** Returns the maximal allowed number of suspended event
     * processing threads (per Web application).
     *
     * <p>It is ignored if the use of the event processing thread
     * is disable ({@link #isEventThreadEnabled}.
     * @see #isEventThreadEnabled
     */
    public int getMaxSuspendedThreads() {
        return _suspThdMax;
    }

    /** Sets whether to use the event processing thread.
     *
     * <p>Default: false (disabled).
     *
     * @exception IllegalStateException if there is suspended thread
     * and use is false.
     * @deprecated as of release 10.0.0. (not recommended to use in production)
     */
    public void enableEventThread(boolean enable) {
        if (!enable && _wapp != null) {
            final UiEngine engine = ((WebAppCtrl) _wapp).getUiEngine();
            if (engine != null) {
                if (engine.hasSuspendedThread())
                    throw new IllegalStateException("Unable to disable due to suspended threads");
            }
        }
        if (enable)
            log.warn("Enable event thread has deprecated!");
        _evtThdEnabled = enable;
    }

    /** Returns whether to use the event processing thread.
     * <p>Default: false (disabled).
     * @deprecated as of release 10.0.0. (not recommended to use in production)
     */
    public boolean isEventThreadEnabled() {
        return _evtThdEnabled;
    }

    /** Sets whether zscript is allowed.
     * <p>Default: true (enabled).
     * @since 6.0.0
     */
    public void enableZScript(boolean enable) {
        _zscriptEnabled = enable;
    }

    /** Returns whether zscript is allowed.
     * <p>Default: true (enabled).
     * @since 6.0.0
     */
    public boolean isZScriptEnabled() {
        return _zscriptEnabled;
    }

    /** Returns the monitor for this application, or null if not set.
     */
    public Monitor getMonitor() {
        return _monitor;
    }

    /** Sets the monitor for this application, or null to disable it.
     *
     * <p>Default: null.
     *
     * <p>There is at most one monitor for each Web application.
     * The previous monitor will be replaced when this method is called.
     *
     * <p>In addition to call this method, you could specify a monitor
     * in zk.xml
     *
     * @param monitor the performance meter. If null, the meter function
     * is disabled.
     * @return the previous monitor, or null if not available.
     */
    public Monitor setMonitor(Monitor monitor) {
        final Monitor old = _monitor;
        _monitor = monitor;
        return old;
    }

    /** Returns the performance meter for this application, or null if not set.
     * @since 3.0.0
     */
    public PerformanceMeter getPerformanceMeter() {
        return _pfmeter;
    }

    /** Sets the performance meter for this application, or null to disable it.
     *
     * <p>Default: null.
     *
     * <p>There is at most one performance meter for each Web application.
     * The previous meter will be replaced when this method is called.
     *
     * <p>In addition to call this method, you could specify
     * a performance meter in zk.xml
     *
     * @param meter the performance meter. If null, the meter function
     * is disabled.
     * @return the previous performance meter, or null if not available.
     * @since 3.0.0
     */
    public PerformanceMeter setPerformanceMeter(PerformanceMeter meter) {
        final PerformanceMeter old = _pfmeter;
        _pfmeter = meter;
        return old;
    }

    /** Returns the execution monitor for this application, or null if not set.
     * @since 6.0.0
     */
    public ExecutionMonitor getExecutionMonitor() {
        return _execmon;
    }

    /** Sets the execution monitor for this application, or null to disable it.
     *
     * <p>Default: null.
     *
     * <p>There is at most one execution monitor for each Web application.
     * The previous meter will be replaced when this method is called.
     *
     * <p>In addition to call this method, you could specify
     * an execution monitor in zk.xml
     *
     * @param monitor the execution monitor. If null, the monitor function
     * is disabled.
     * @return the previous execution monitor, or null if not available.
     * @since 6.0.0
     */
    public ExecutionMonitor setExecutionMonitor(ExecutionMonitor monitor) {
        final ExecutionMonitor old = _execmon;
        _execmon = monitor;
        return old;
    }

    /** Returns the desktop recycle for this application, or null if not set.
     * @since 5.0.0
     */
    public DesktopRecycle getDesktopRecycle() {
        return _dtRecycle;
    }

    /** Sets the desktop recycler for this application, or null to disable it.
     *
     * <p>Default: null.
     *
     * <p>There is at most one desktop recycle for each Web application.
     * The previous instance will be replaced when this method is called.
     *
     * <p>In addition to call this method, you could specify
     * a desktop recycle in zk.xml
     *
     * @param dtRecycle the desktop recycle. If null, the recycle function
     * is disabled.
     * @return the previous desktop recycle, or null if not available.
     * @since 5.0.0
     */
    public DesktopRecycle setDesktopRecycle(DesktopRecycle dtRecycle) {
        final DesktopRecycle old = _dtRecycle;
        _dtRecycle = dtRecycle;
        return old;
    }

    /** Returns the charset used to generate the HTTP response
     * or null to use the container's default.
     * It is currently used by {@link org.zkoss.zk.ui.http.DHtmlLayoutServlet},
     *
     * <p>Default: UTF-8.
     */
    public String getResponseCharset() {
        return _charsetResp;
    }

    /** Sets the charset used to generate HTTP response.
     * It is currently used by {@link org.zkoss.zk.ui.http.DHtmlLayoutServlet},
     *
     * @param charset the charset to use. If null or empty, the container's default
     * is used.
     */
    public void setResponseCharset(String charset) {
        _charsetResp = charset != null && charset.length() > 0 ? charset : null;
    }

    /** Returns the value of the preference defined in zk.xml, or by
     * {@link #setPreference}.
     *
     * <p>Preference is application specific. You can specify whatever you want
     * as you specifying context-param for a Web application.
     *
     * @param defaultValue the default value that is used if the specified
     * preference is not found.
     */
    public String getPreference(String name, String defaultValue) {
        final String value = _prefs.get(name);
        return value != null ? value : defaultValue;
    }

    /** Sets the value of the preference.
     */
    public void setPreference(String name, String value) {
        if (name == null || value == null)
            throw new IllegalArgumentException("null");
        _prefs.put(name, value);
    }

    /** Returns a readonly set of all preference names.
     */
    public Set<String> getPreferenceNames() {
        return _prefs.keySet();
    }

    /** Adds the definition of a richlet.
     *
     * <p>If there was a richlet associated with the same name, the
     * the old richlet will be replaced.
     *
     * @param name the richlet name
     * @param params the initial parameters, or null if no initial parameter at all.
     * Once called, the caller cannot access <code>params</code> any more.
     * @return the previous richlet class or class-name with the specified name,
     * or null if no previous richlet.
     */
    public Object addRichlet(String name, Class<?> richletClass, Map<String, String> params) {
        if (!Richlet.class.isAssignableFrom(richletClass))
            throw new IllegalArgumentException(
                    "A richlet class, " + richletClass + ", must implement " + Richlet.class.getName());

        return addRichlet0(name, richletClass, params);
    }

    /** Adds the definition of a richlet.
     *
     * <p>If there was a richlet associated with the same name, the
     * the old servlet will be replaced.
     *
     * @param name the richlet name
     * @param richletClassName the class name. The class will be loaded
     * only when the richlet is loaded.
     * @param params the initial parameters, or null if no initial parameter at all.
     * Once called, the caller cannot access <code>params</code> any more.
     * @return the previous richlet class or class-name with the specified name,
     * or null if no previous richlet.
     */
    public Object addRichlet(String name, String richletClassName, Map<String, String> params) {
        if (richletClassName == null || richletClassName.length() == 0)
            throw new IllegalArgumentException("richletClassName is required");

        return addRichlet0(name, richletClassName, params);
    }

    /** Adds the richlet.
     *
     * <p>If there was a richlet associated with the same name, the
     * the old one will be replaced.
     *
     * @param name the richlet name
     * @param richlet the richlet implemetation.
     * @return the previous richlet class or class-name with the specified name,
     * or null if no previous richlet.
     * @since 7.0.2
     */
    public Object addRichlet(String name, Richlet richlet) {
        if (richlet == null)
            throw new IllegalArgumentException("richlet instance is required");

        return addRichlet0(name, richlet, null);
    }

    private Object addRichlet0(String name, Object richletClass, Map<String, String> params) {
        Object o;

        for (;;) {
            // remove previous richlet if it exists
            o = removeRichlet0(name);

            synchronized (_richlets) {
                // add new richlet definition only if map does not contain record
                // with same name
                if (!(_richlets.containsKey(name))) {
                    if (richletClass instanceof Richlet) {
                        _richlets.put(name, richletClass);
                    } else {
                        _richlets.put(name, new Object[] { richletClass, params });
                    }
                    break;
                }
            }
        }

        return o;
    }

    /**
     * Removes the richlet and associated richlet mappings.
     * 
     * @param name the richlet name
     * @return the removed richlet class or class-name with the specified name,
     * or null if the richlet is not found.
     * @since 7.0.2
     */
    public Object removeRichlet(String name) {
        // remove richlet
        final Object o = removeRichlet0(name);

        // remove associated richlet mappings
        removeRichletMapping(name);

        return o;
    }

    /**
     * Removes the richlet.
     * 
     * @param name the richlet name
     * @return the removed richlet class or class-name with the specified name,
     * or null if the richlet is not found.
     * @since 7.0.2
     */
    private Object removeRichlet0(String name) {
        if (name == null) {
            throw new IllegalArgumentException("Name is required");
        }
        Object o;

        for (;;) {
            // remove richlet
            synchronized (_richlets) {
                o = _richlets.remove(name);
            }

            // verify it sth instancing richlet at the moment
            if (o instanceof WaitLock) {
                WaitLock lock = (WaitLock) o;
                if (!lock.waitUntilUnlock(300 * 1000)) { //5 minute
                    String msg = new StringBuilder("Unable to remove richlet ").append(name)
                            .append("\nCause: conflict too long.").toString();
                    final PotentialDeadLockException ex = new PotentialDeadLockException(msg);
                    log.warn(msg, ex); //very rare, possibly a bug
                    throw ex;
                }
            } else {
                break;
            }
        }

        if (o == null) {
            return null;
        }
        if (o instanceof Richlet) {
            // destroy object if it is richlet
            destroy((Richlet) o);
            return o.getClass();
        }
        return ((Object[]) o)[0];
    }

    /** Adds a richlet mapping.
     *
     * @param name the name of the richlet.
     * @param path the URL pattern. It must start with '/' and may end
     * with '/*'.
     * @exception UiException if the richlet is not defined yet.
     * See {@link #addRichlet}.
     * @since 2.4.0
     */
    public void addRichletMapping(String name, String path) {
        //Note: "/" is the same as ""
        if (path == null || path.length() == 0 || "/".equals(path))
            path = "";
        else if (path.charAt(0) != '/')
            throw new IllegalArgumentException("path must start with '/', not " + path);

        final boolean wildcard = path.endsWith("/*");
        if (wildcard) //wildcard
            path = path.substring(0, path.length() - 2);
        //note it might be empty

        //richlet mapping cannot be added if richlet is not defined,
        //so check if richlet with same name exists and then
        //add richlet mapping
        synchronized (_richlets) {
            if (!_richlets.containsKey(name))
                throw new UiException("Richlet not defined: " + name);

            synchronized (_richletmaps) {
                _richletmaps.put(path, new Object[] { name, Boolean.valueOf(wildcard) });
            }
        }
    }

    /**
     * Removes all richlet mappings for the specified richlet.
     * 
     * @param name the richlet name
     */
    private void removeRichletMapping(String name) {
        // remove richlet mapping
        synchronized (_richletmaps) {
            Iterator<Map.Entry<String, Object[]>> iter = _richletmaps.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, Object[]> entry = iter.next();
                String richletName = (String) entry.getValue()[0];
                if (richletName.equals(name)) {
                    iter.remove();
                }
            }
        }
    }

    private static void destroy(Richlet richlet) {
        try {
            richlet.destroy();
        } catch (Throwable ex) {
            log.error("Unable to destroy " + richlet);
        }
    }

    /** Returns an instance of richlet of the specified name, or null
     * if not found.
     */
    @SuppressWarnings("unchecked")
    public Richlet getRichlet(String name) {
        WaitLock lock = null;
        final Object[] info;
        for (;;) {
            synchronized (_richlets) {
                Object o = _richlets.get(name);
                if (o == null || (o instanceof Richlet)) { //not found or loaded
                    return (Richlet) o;
                } else if (o instanceof WaitLock) { //loading by another thread
                    lock = (WaitLock) o;
                } else {
                    info = (Object[]) o;

                    //going to load in this thread
                    _richlets.put(name, lock = new WaitLock());
                    break; //then, load it
                }
            } //sync(_richlets)

            if (!lock.waitUntilUnlock(300 * 1000)) { //5 minute
                final PotentialDeadLockException ex = new PotentialDeadLockException(
                        "Unable to load richlet " + name + "\nCause: conflict too long.");
                log.warn("", ex); //very rare, possibly a bug
                throw ex;
            }
        } //for (;;)

        //load it
        try {
            if (info[0] instanceof String) {
                try {
                    info[0] = Classes.forNameByThread((String) info[0]);
                } catch (Throwable ex) {
                    throw new UiException("Failed to load " + info[0]);
                }
            }

            final Object o = ((Class<?>) info[0]).newInstance();
            if (!(o instanceof Richlet))
                throw new UiException(Richlet.class + " must be implemented by " + info[0]);

            final Richlet richlet = (Richlet) o;
            richlet.init(newRichletConfig((Map<String, String>) info[1]));

            synchronized (_richlets) {
                _richlets.put(name, richlet);
            }
            return richlet;
        } catch (Throwable ex) {
            synchronized (_richlets) {
                _richlets.put(name, info); //remove lock and restore info
            }
            throw UiException.Aide.wrap(ex, "Unable to instantiate " + info[0]);
        } finally {
            lock.unlock();
        }
    }

    private RichletConfig newRichletConfig(Map<String, String> params) {
        return new RichletConfigImpl(_wapp, params);
    }

    /** Returns an instance of richlet for the specified path, or
     * null if not found.
     */
    public Richlet getRichletByPath(String path) {
        if (path == null || path.length() == 0 || "/".equals(path))
            path = "";
        else if (path.charAt(0) != '/')
            path = '/' + path;

        final int len = path.length();
        for (int j = len;;) {
            final Richlet richlet = getRichletByPath0(path.substring(0, j), j != len);
            if (richlet != null || j == 0)
                return richlet;
            j = path.lastIndexOf('/', j - 1); //j must not -1
        }
    }

    private Richlet getRichletByPath0(String path, boolean wildcardOnly) {
        final Object[] info;
        synchronized (_richletmaps) {
            info = _richletmaps.get(path);
        }
        return info != null && (!wildcardOnly || ((Boolean) info[1]).booleanValue()) ? getRichlet((String) info[0])
                : null;
    }

    /** Destroys all richlets.
     */
    public void detroyRichlets() {
        synchronized (_richlets) {
            for (Iterator<Object> it = _richlets.values().iterator(); it.hasNext();) {
                final Object o = it.next();
                if (o instanceof Richlet)
                    destroy((Richlet) o);
            }
            _richlets.clear();
        }
    }

    /** Specifies whether to keep the desktops across visits.
     * If false, the desktops are removed when an user reloads an URL
     * or browses to another URL.
     *
     * <p>Default: false.
     */
    public void setKeepDesktopAcrossVisits(boolean keep) {
        _keepDesktop = keep;
    }

    /** Returns whether to keep the desktops across visits.
     * If false, the desktops are removed when an user reloads an URL
     * or browses to another URL.
     */
    public boolean isKeepDesktopAcrossVisits() {
        return _keepDesktop;
    }

    /** Specifies whether to keep the session alive,
     * when receiving the onTimer event.
     *
     * <p>Default: false.
     *
     * <p>A session is expired (and then invalidated), if it didn't receive
     * any client request in the specified timeout interval
     * ({@link #getSessionMaxInactiveInterval}).
     * By setting this option to true, the session timeout will be reset
     * when onTimer is received (just like any other event).
     *
     * <p>Note: if true and the timer is shorter than
     * the session timeout ({@link #getSessionMaxInactiveInterval}),
     * the session is never expired.
     *
     * @param alive whether to keep the session alive when receiving
     * onTimer
     * @since 3.0.0
     */
    public void setTimerKeepAlive(boolean alive) {
        _timerKeepAlive = alive;
    }

    /** Returns whether to keep the session alive,
     * when receiving the onTimer event.
     * In other words, it returns whether to reset the session timeout
     * counter when receiving onTimer, just like any other events.
     *
     * @since 3.0.0
     */
    public boolean isTimerKeepAlive() {
        return _timerKeepAlive;
    }

    /** Returns whether to debug JavaScript files.
     * If true, it means the original (i.e., uncompressed) JavaScript files
     * shall be loaded instead of the compressed JavaScript files.
     *
     * @since 3.0.4
     * @see #setDebugJS
     */
    public boolean isDebugJS() {
        return _debugJS;
    }

    /**Sets whether to debug JavaScript files.
     *
     * <p>Default: false.
     *
     * <p>If true is specified, it will try to load the original
     * Java (i.e., uncompressed) file instead of the compressed one.
     * For example, if {@link org.zkoss.web.util.resource.ClassWebResource#service} is called to load abc.js,
     * and {@link #isDebugJS}, then {@link org.zkoss.web.util.resource.ClassWebResource#service} will try
     * to load abc.org.js first. If not found, it load ab.js insted.
     *
     * <p>If {@link #isDebugJS} is false (default),
     * abc.js is always loaded.
     *
     * <p>Prior to 5.0.3, the setting won't affect JavaScript files that have been
     * loaded. That is, the reboot is required.
     *
     * @param debug whether to debug JavaScript files.
     * If true, the original JavaScript files shall be
     * loaded instead of the compressed files.
     * @since 3.0.4
     */
    public void setDebugJS(boolean debug) {
        _debugJS = debug;
        if (_wapp != null)
            org.zkoss.zk.ui.http.Utils.updateDebugJS(_wapp, debug);
    }

    /** Sets the implementation of the expression factory that shall
     * be used by the whole system.
     *
     * <p>Default: null -- it means the org.zkoss.xel.el.ELFactory class
     * (it requires zcommons-el.jar).
     *
     * <p>Note: you can only specify an implementation that is compatible
     * with JSP EL here, since ZK's builtin pages depend on it.
     * However, you can use any factory you like in an individual page,
     * as long as all expressions in the page follow the syntax of
     * the evaluator you are using.
     *
     * @param expfcls the implemtation class, or null to use the default.
     * Note: expfcls must implement {@link ExpressionFactory}.
     * @since 3.0.0
     */
    public void setExpressionFactoryClass(Class<? extends ExpressionFactory> expfcls) {
        Expressions.setExpressionFactoryClass(expfcls);
    }

    /** Returns the implementation of the expression factory that
     * is used by the whole system, or null if the system default is used.
     *
     * @see #setExpressionFactoryClass
     * @since 3.0.0
     */
    public Class<? extends ExpressionFactory> getExpressionFactoryClass() {
        return Expressions.getExpressionFactoryClass();
    }

    /** Invokes {@link EventInterceptor#beforeSendEvent}
     * registered by {@link #addListener} with a class implementing
     * {@link EventInterceptor}.
     * <p>Used only internally.
     * @since 3.0.0
     */
    public Event beforeSendEvent(Event event) {
        return _eis.beforeSendEvent(event);
    }

    /** Invokes {@link EventInterceptor#beforePostEvent}
     * registered by {@link #addListener} with a class implementing
     * {@link EventInterceptor}.
     * <p>Used only internally.
     * @since 3.0.0
     */
    public Event beforePostEvent(Event event) {
        return _eis.beforePostEvent(event);
    }

    /** Invokes {@link EventInterceptor#beforeProcessEvent}
     * registered by {@link #addListener} with a class implementing
     * {@link EventInterceptor}.
     * <p>Used only internally.
     * @since 3.0.0
     */
    public Event beforeProcessEvent(Event event) {
        return _eis.beforeProcessEvent(event);
    }

    /** Invokes {@link EventInterceptor#afterProcessEvent}
     * registered by {@link #addListener} with a class implementing
     * {@link EventInterceptor}.
     * <p>Used only internally.
     * @since 3.0.0
     */
    public void afterProcessEvent(Event event) {
        _eis.afterProcessEvent(event);
    }

    /** Returns a map of application-specific attributes.
     * @since 5.0.0
     */
    public Map<String, Object> getAttributes() {
        return _attrs;
    }

    /** Returns the value of an application-specific attribute, or
     * null if not found.
     * @since 5.0.0
     */
    public Object getAttribute(String name) {
        return _attrs.get(name);
    }

    /** Returns the value of an application-specific attribute.
     * @param value the value of the attribute. If null, it means removal,
     * i.e., {@link #removeAttribute}.
     * @return the previous value, or null if no such value.
     * @since 5.0.0
     */
    public Object setAttribute(String name, Object value) {
        return value != null ? _attrs.put(name, value) : removeAttribute(name);
    }

    /** Removes the value of an application-specific attribute.
     * @return the previous value, or null if no such value.
     * @since 5.0.0
     */
    public Object removeAttribute(String name) {
        return _attrs.remove(name);
    }

    /** Adds a client (JavaScript) package that is provided by this server.
     * <p>Default: none.
     * <p>If no package is defined (default), ZK Client Engine assumes
     * all packages coming from the server generating the HTML page.
     *
     * <p>However, it might not be true if you want to load some client
     * codes from different server (such as Ajax-asService).
     * Therefore, you have to invoke this method to add the client packages
     * if this server is going to provide JavaScript codes for other servers.
     * @since 5.0.0
     */
    public void addClientPackage(String pkg) {
        if (pkg == null || pkg.length() == 0)
            throw new IllegalArgumentException("empty");
        _clientpkgs.add(pkg);
    }

    /** Returns a readonly list of the names of the client pages
     * that are provided by this server
     *
     * @since 5.0.0
     */
    public String[] getClientPackages() {
        return _clientpkgs.toArray();
    }

    /** Returns the time, in seconds, to show a warning message
     * if an event has been processinged longer than it.
     * <p>Default: 600
     * @since 3.6.3
     */
    public int getEventTimeWarning() {
        return _evtTimeWarn;
    }

    /** Set the time, in seconds, to show a warning message
     * if an event has been processinged longer than it.
     * @param secs the number of seconds.
     * If a non-positive number is specified, no warning message at all.
     * @since 3.6.3
     */
    public void setEventTimeWarning(int secs) {
        _evtTimeWarn = secs;
    }

    /** Adds an error page.
     *
     * @param deviceType the device type: ajax or mil
     * @param type what type of errors the error page is associated with.
     * @param location where is the error page.
     * @return the previous location of the same error, or null if not
     * defined yet.
     * @since 2.4.1
     */
    public String addErrorPage(String deviceType, Class<?> type, String location) {
        if (!Throwable.class.isAssignableFrom(type))
            throw new IllegalArgumentException("Throwable or derived is required: " + type);
        if (location == null || deviceType == null)
            throw new IllegalArgumentException();

        List<ErrorPage> l;
        synchronized (_errpgs) {
            l = _errpgs.get(deviceType);
            if (l == null)
                _errpgs.put(deviceType, l = new LinkedList<ErrorPage>());
        }

        String previous = null;
        synchronized (l) {
            //remove the previous definition
            for (Iterator<ErrorPage> it = l.iterator(); it.hasNext();) {
                final ErrorPage errpg = it.next();
                if (errpg.type.equals(type)) {
                    previous = errpg.location;
                    it.remove();
                    break;
                }
            }
            l.add(new ErrorPage(type, location));
        }
        return previous;
    }

    /** Returns the error page that matches the specified error, or null if not found.
     *
     * @param deviceType the device type: ajax or mil
     * @param error the exception being thrown
     * @since 2.4.1
     */
    public String getErrorPage(String deviceType, Throwable error) {
        if (!_errpgs.isEmpty()) {
            final List<ErrorPage> l;
            synchronized (_errpgs) {
                l = _errpgs.get(deviceType);
            }
            if (l != null) {
                synchronized (l) {
                    for (Iterator<ErrorPage> it = l.iterator(); it.hasNext();) {
                        final ErrorPage errpg = it.next();
                        if (errpg.type.isInstance(error))
                            return errpg.location;
                    }
                }
            }
        }
        return null;
    }

    private static class ErrorPage {
        private final Class<?> type;
        private final String location;

        private ErrorPage(Class<?> type, String location) {
            this.type = type;
            this.location = location;
        }
    }

    /** Used with {@link FastReadArray} to check if an object is
     * the same class as specified.
     */
    private static class SameClass implements Comparable<Object> {
        private final Class<?> _klass;

        private SameClass(Class<?> klass) {
            _klass = klass;
        }

        public int compareTo(Object o) {
            return o.getClass().equals(_klass) ? 0 : 1;
        }

        //Object//
        public String toString() {
            return Objects.toString(_klass);
        }

        public boolean equals(Object o) {
            if (this == o)
                return true;
            return Objects.equals(_klass, o instanceof SameClass ? ((SameClass) o)._klass : o);
        }

        public int hashCode() {
            return Objects.hashCode(_klass);
        }
    }

    private static class TimeoutURIInfo extends URIInfo {
        private String message;
        private boolean auto;

        private TimeoutURIInfo() {
            super(null);
        }

        private TimeoutURIInfo(String uri, int type) {
            super(uri, type);
        }
    }

    /**
     * Returns whether to use custom {@link ThemeProvider}. If true, it means
     * the default ThemeProvider shall be ignored. It is set to true if declare
     * custom ThemeProvider in zk.xml
     * <p>
     * Default: false
     * @since 7.0.0
     */
    public boolean isCustomThemeProvider() {
        return _customThemeProvider;
    }

    /**
     * Sets whether to use custom {@link ThemeProvider}.
     * <p>Default: false.
     * <p>It is set to true if declare custom ThemeProvider in zk.xml
     * <p>Note: internal use only
     * @param customThemeProvider whether to use custom {@link ThemeProvider}.
     * @since 7.0.0
     */
    public void setCustomThemeProvider(boolean customThemeProvider) {
        _customThemeProvider = customThemeProvider;
    }

    /**
     * Returns whether to use custom {@link ThemeRegistry}. If true, it means
     * the default ThemeRegistry shall be ignored. It is set to true if declare
     * custom ThemeRegistry in zk.xml
     * <p>Default: false
     * @since 7.0.0
     */
    public boolean isCustomThemeRegistry() {
        return _customThemeRegistry;
    }

    /**
     * Sets whether to use custom {@link ThemeRegistry}.
     * <p>Default: false.
     * <p>It is set to true if declare custom ThemeRegistry in zk.xml.
     * <p>Note: internal use only
     * @param customThemeRegistry whether to use custom {@link ThemeRegistry}.
     * @since 7.0.0
     */
    public void setCustomThemeRegistry(boolean customThemeRegistry) {
        _customThemeRegistry = customThemeRegistry;
    }

    /**
     * Returns whether to use custom {@link ThemeResolver}. If true, it means
     * the default ThemeResolver shall be ignored. It is set to true if declare
     * custom ThemeResolver in zk.xml
     * <p>Default: false
     * @since 7.0.0
     */
    public boolean isCustomThemeResolver() {
        return _customThemeResolver;
    }

    /**
     * Sets whether to use custom {@link ThemeResolver}.
     * <p>Default: false.
     * <p>It is set to true if declare custom ThemeResolver in zk.xml.
     * <p>Note: internal use only
     * @param customThemeResolver whether to use custom {@link ThemeResolver}.
     * @since 7.0.0
     */
    public void setCustomThemeResolver(boolean customThemeResolver) {
        _customThemeResolver = customThemeResolver;
    }

    /**
     * Sets user customized init crash script
     * <p>User can customize init crash page layout by defining a javascript function which is assigned to <code>window.zkShowCrashMessage</code>
     * @param script
     * @since 7.0.4
     */
    public void setInitCrashScript(String script) {
        _initCrashScript = script;
    }

    /**
     * Sets user customized init crash timeout
     * <p>User can customize init crash timeout by simply giving a number(sec).
     * @param timeout
     * @since 7.0.4
     */
    public void setInitCrashTimeout(int timeout) {
        _initCrashTimeout = timeout;
    }

    /**
     * Returns init crash script, if null, use default, see crashmsg.js
     * @return String 
     * @since 7.0.4
     */
    public String getInitCrashScript() {
        return _initCrashScript;
    }

    /**
     * Returns init crash timeout, if -1, use default, which is 60 sec
     * @return int
     * @since 7.0.4
     */
    public int getInitCrashTimeout() {
        return _initCrashTimeout;
    }

    /**
     * Returns binder init attribute name
     * @return String
     * @since 8.0.0
     */
    public String getBinderInitAttribute() {
        return _binderInitAttribute;
    }

    /**
     * Set binder init attribute name
     * @since 8.0.0
     */
    public void setBinderInitAttribute(String binderInitAttribute) {
        this._binderInitAttribute = binderInitAttribute;
    }

    /**
     * Returns binder annotations
     * @return Set
     * @since 8.0.0
     */
    public Set<String> getBinderAnnotations() {
        return _binderAnnotations;
    }

    /**
     * Set binder annotations 
     * @since 8.0.0
     */
    public void setBinderAnnotations(Set<String> binderAnnotations) {
        this._binderAnnotations = new HashSet<String>(binderAnnotations);
    }

    /**
     * Adds client data attribute handler
     * @since 8.0.0
     */
    public void addDataHandler(DataHandlerInfo info) {
        final String name = info.getName();
        DataHandlerInfo old = _dataHandlers.put(name, info);
        if (info.isOverride()) {
            if (old == null)
                log.warn("The data handler cannot be overridden! Not existing: [" + name + "]");
        } else {
            if (old != null)
                log.warn("The data handler has been defined! [" + name
                        + "]\n Please use <override>true</override> to disable the warning.");
        }
    }

    /**
     * Returns all of the client data attribute handlers
     * @since
     */
    public Map<String, DataHandlerInfo> getDataHandlers() {
        return _dataHandlers;
    }

    /** Sets whether source map is enabled.
     * <p>Default: false (disabled).
     * @since 8.5.0
     */
    public void enableSourceMap(boolean enable) {
        _sourceMapEnabled = enable;
    }

    /** Returns whether source map is enabled.
     * <p>Default: false (disabled).
     * @since 8.5.0
     */
    public boolean isSourceMapEnabled() {
        return _sourceMapEnabled;
    }

    /** Sets whether history state handle is enabled.
     * If disabled, ZK won't generate any onHistoryPopState event even if history state is popped.
     * It is desirable to avoid unnecessary events back to server if history state is handled by front-end.
     * <p>Default: true (enabled).
     * @since 8.6.1
     */
    public void enableHistoryState(boolean enable) {
        _historyStateEnabled = enable;
    }

    /** Returns whether history state handle is enabled.
     * <p>Default: true (enabled).
     * @since 8.6.1
     */
    public boolean isHistoryStateEnabled() {
        return _historyStateEnabled;
    }

    /**
     * Sets whether to send client errors to the server for logging
     * the page url where the error occurred and its stack trace.
     * <p>Default: false.</p>
     * @since 10.0.0
     */
    public void setSendClientErrors(boolean send) {
        _sendClientErrors = send;
    }

    /**
     * Returns whether to send client errors to the server for logging
     * the page url where the error occurred and its stack trace.
     * <p>Default: false.</p>
     * @since 10.0.0
     */
    public boolean isSendClientErrors() {
        return _sendClientErrors;
    }
}