zk/src/main/java/org/zkoss/zk/ui/sys/ConfigParser.java

Summary

Maintainability
F
1 wk
Test Coverage
/* ConfigParser.java

    Purpose:
        
    Description:
        
    History:
        Sun Mar 26 18:09:10     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.sys;

import static org.zkoss.lang.Generics.cast;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URL;
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.idom.Attribute;
import org.zkoss.idom.Document;
import org.zkoss.idom.Element;
import org.zkoss.idom.input.SAXBuilder;
import org.zkoss.idom.util.IDOMs;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Strings;
import org.zkoss.mesg.MCommon;
import org.zkoss.util.Cache;
import org.zkoss.util.IllegalSyntaxException;
import org.zkoss.util.Pair;
import org.zkoss.util.Utils;
import org.zkoss.util.resource.Locator;
import org.zkoss.util.resource.XMLResourcesLocator;
import org.zkoss.web.fn.ThemeFns;
import org.zkoss.web.theme.ThemeRegistry;
import org.zkoss.web.theme.ThemeResolver;
import org.zkoss.xel.ExpressionFactory;
import org.zkoss.zk.au.AuDecoder;
import org.zkoss.zk.au.AuWriter;
import org.zkoss.zk.au.AuWriters;
import org.zkoss.zk.device.Devices;
import org.zkoss.zk.scripting.Interpreters;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.metainfo.DefinitionLoaders;
import org.zkoss.zk.ui.util.CharsetFinder;
import org.zkoss.zk.ui.util.Configuration;
import org.zkoss.zk.ui.util.DataHandlerInfo;
import org.zkoss.zk.ui.util.ThemeProvider;
import org.zkoss.zk.ui.util.ThemeURIHandler;
import org.zkoss.zk.ui.util.URIInfo;

/**
 * Used to parse WEB-INF/zk.xml, metainfo/zk/zk.xml 
 * and metainfo/zk/config.xml into {@link Configuration}.
 *
 * @author tomyeh
 */
public class ConfigParser {
    private static final Logger log = LoggerFactory.getLogger(ConfigParser.class);

    /** The number of segments in a version.
     */
    private static final int MAX_VERSION_SEGMENT = 4;
    private static int[] _zkver;
    private static List<org.zkoss.zk.ui.util.ConfigParser> _parsers;
    // Map<int, boolean>: whether an instance of Configuration
    private static final Map<Integer, Boolean> _syscfgLoadedConfigs = new HashMap<Integer, Boolean>(4);
    private static boolean _syscfgLoaded;

    /** Checks and returns whether the loaded document's version is correct.
     * It is the same as checkVersion(url, doc, false).
     * @since 3.5.0
     */
    public static boolean checkVersion(URL url, Document doc) throws Exception {
        return checkVersion(url, doc, false);
    }

    /** Checks and returns whether the loaded document's version is correct.
     * @param zk5required whether ZK 5 or later is required.
     * If true and zk-version is earlier than 5, doc will be ignored
     * (and false is returned).
     * @since 5.0.0
     */
    public static boolean checkVersion(URL url, Document doc, boolean zk5required) throws Exception {
        final Element el = doc.getRootElement().getElement("version");
        if (el == null)
            return true; //version is optional (3.0.5)

        if (_zkver == null)
            _zkver = Utils.parseVersion(org.zkoss.zk.Version.UID);

        String s = el.getElementValue("zk-version", true);
        if (s != null) {
            final int[] reqzkver = Utils.parseVersion(s);
            if (Utils.compareVersion(_zkver, reqzkver) < 0) {
                log.info("Ignore " + url + "\nCause: ZK version must be " + s + " or later, not " + org.zkoss.zk.Version.UID);
                return false;
            }
            if (zk5required && reqzkver.length > 0 && reqzkver[0] < 5) {
                log.info("Ingore " + url + "\nCause: version " + s + " not supported");
                return false;
            }
        }

        final String clsnm = el.getElementValue("version-class", true);
        if (clsnm == null) {
            return true; //version is optional 3.0.5
        }

        if (clsnm.length() == 0)
            log.warn("Ignored: empty version-class, " + el.getLocator());

        final String uid = IDOMs.getRequiredElementValue(el, "version-uid");
        final Class cls = Classes.forNameByThread(clsnm);
        final Field fld = cls.getField("UID");
        final String uidInClass = (String) fld.get(null);
        if (!uid.equals(uidInClass)) {
            log.info("Ignore " + url + "\nCause: version not matched; expected=" + uidInClass + ", xml=" + uid);
            return false;
        }

        return true; //matched
    }

    /** Used to provide backward compatibility to 2.3.0's richlet definition. */
    private int _richletnm;

    /** Parses metainfo/zk/config.xml placed in class-path.
     *
     * <p>Note: the application-independent configurations (a.k.a.,
     * the system default configurations) are loaded only once,
     * no matter how many times this method is called.
     *
     * @param config the object to store configurations.
     * If null, only the application-independent configurations are parsed.
     * @since 3.5.0
     */
    public void parseConfigXml(Configuration config) {
        boolean syscfgLoaded;
        boolean syscfgLoadedConfig;
        synchronized (ConfigParser.class) {
            syscfgLoaded = _syscfgLoaded;
            _syscfgLoaded = true;
            syscfgLoadedConfig = config != null ? _syscfgLoadedConfigs.put(new Integer(System.identityHashCode(config)),
                    //chance of two instances with same code is almost zero
                    Boolean.TRUE) != null : syscfgLoaded;
        }
        if (!syscfgLoaded)
            log.info("Loading system default");
        else if (config == null)
            return; //nothing to do

        try {
            final XMLResourcesLocator locator = org.zkoss.zk.ui.impl.Utils.getXMLResourcesLocator();
            final List<XMLResourcesLocator.Resource> xmls = locator.getDependentXMLResources("metainfo/zk/config.xml",
                    "config-name", "depends");
            for (XMLResourcesLocator.Resource res : xmls) {
                if (log.isDebugEnabled())
                    log.debug("Loading " + res.url);
                try {
                    if (checkVersion(res.url, res.document)) {
                        final Element el = res.document.getRootElement();
                        if (!syscfgLoaded) {
                            parseSubZScriptConfig(el);
                            parseSubDeviceConfig(el);
                        }
                        if (!syscfgLoadedConfig) { //config not null
                            parseSubSystemConfig(config, el);
                            parseSubClientConfig(config, el);
                        }
                        if (!syscfgLoaded) {
                            parseProperties(el);
                            parseLangConfigs(locator, el);
                        }

                        if (config != null) {
                            parseListeners(config, el);
                            parsePreferences(config, el);
                            //F80 - store subtree's binder annotation count
                            if (config.getBinderInitAttribute() == null
                                    && "zkbind".equals(el.getElement("config-name").getText()))
                                parseBinderConfig(config, el);
                        }
                    }
                } catch (Exception ex) {
                    throw UiException.Aide.wrap(ex, "Failed to load " + res.url);
                    //abort since it is hardly to work then
                }
            }
        } catch (java.io.IOException ex) {
            throw UiException.Aide.wrap(ex); //abort
        }
    }

    private static void parseSubZScriptConfig(Element root) {
        for (Iterator it = root.getElements("zscript-config").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            Interpreters.add(el);
            //Note: zscript-config is applied to the whole system, not just langdef
        }
    }

    private static void parseSubDeviceConfig(Element root) {
        for (Iterator it = root.getElements("device-config").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            Devices.add(el);
        }
    }

    private static void parseSubSystemConfig(Configuration config, Element root) throws Exception {
        for (Element el : root.getElements("system-config")) {
            if (config != null) {
                parseSystemConfig(config, el);
            } else {
                Class<org.zkoss.zk.ui.util.ConfigParser> cls = parseClass(el, "au-writer-class", AuWriter.class);
                if (cls != null)
                    AuWriters.setImplementationClass(cls);
                cls = parseClass(el, "config-parser-class", org.zkoss.zk.ui.util.ConfigParser.class);
                if (cls != null) {
                    if (_parsers == null)
                        _parsers = new LinkedList<org.zkoss.zk.ui.util.ConfigParser>();
                    _parsers.add(cls.newInstance());
                }
            }
        }
    }

    /** Unlike other private parseXxx, config might be null. */
    private static void parseSubClientConfig(Configuration config, Element root) throws Exception {
        for (Element el : root.getElements("client-config")) {
            if (config != null) {
                parseClientConfig(config, el);
            }
        }
    }

    private static void parseListeners(Configuration config, Element root) throws Exception {
        for (Element el : root.getElements("listener")) {
            parseListener(config, el);
        }
    }

    private static void parseListener(Configuration config, Element el) {
        try {
            config.addListener(parseClass(el, "listener-class", null, true));
        } catch (Exception ex) {
            log.error("Unable to load a listener, " + el.getLocator(), ex);
        }
    }

    /** Parses zk.xml, specified by url, into the configuration.
     *
     * @param url the URL of zk.xml.
     */
    public void parse(URL url, Configuration config, Locator locator) throws Exception {
        if (url == null || config == null)
            throw new IllegalArgumentException("null");
        log.info("Parsing " + url);
        parse(new SAXBuilder(true, false, true).build(url).getRootElement(), config, locator);
    }

    /** Parses zk.xml from an input stream into the configuration.
     * @param is the input stream of zk.xml
     * @since 5.0.7
     */
    public void parse(InputStream is, Configuration config, Locator locator) throws Exception {
        if (is == null || config == null)
            throw new IllegalArgumentException("null");
        parse(new SAXBuilder(true, false, true).build(is).getRootElement(), config, locator);
    }

    /** Parses zk.xml, specified by the root element.
     * @since 3.0.1
     */
    public void parse(Element root, Configuration config, Locator locator) throws Exception {
        l_out: for (Iterator it = root.getElements().iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            final String elnm = el.getName();
            // B65-ZK-1671: ThemeProvider specified in metainfo/zk/zk.xml may get overridden by default
            //   config-name/depends elements were introduced to enforce that default configurations are  
            //   loaded in the sequence of zul -> zkex -> zkmax. User-supplied ThemeProvider, ThemeRegistry,
            //   and ThemeResolver will not get overridden by using flag variables. But multiple such
            //   configurations in different metainfo/zk/zk.xml still needs to be resolved by the assistance
            //   of config-name/depends.
            if ("config-name".equals(elnm) || "depends".equals(elnm))
                // known elements; not actual config items
                continue;
            else if ("listener".equals(elnm)) {
                parseListener(config, el);
            } else if ("richlet".equals(elnm)) {
                final String clsnm = IDOMs.getRequiredElementValue(el, "richlet-class");
                final Map<String, String> params = IDOMs.parseParams(el, "init-param", "param-name", "param-value");

                 //syntax since 2.4.0
                final String nm = IDOMs.getRequiredElementValue(el, "richlet-name");
                try {
                    config.addRichlet(nm, clsnm, params);
                } catch (Throwable ex) {
                    log.error("Illegal richlet definition at " + el.getLocator(), ex);
                }
            } else if ("richlet-mapping".equals(elnm)) { //syntax since 2.4.0
                final String nm = IDOMs.getRequiredElementValue(el, "richlet-name");
                final String path = IDOMs.getRequiredElementValue(el, "url-pattern");
                try {
                    config.addRichletMapping(nm, path);
                } catch (Throwable ex) {
                    log.error("Illegal richlet mapping at " + el.getLocator(), ex);
                }
            } else if ("desktop-config".equals(elnm)) {
                //desktop-config
                //    desktop-timeout
                //  disable-theme-uri
                //    file-check-period
                //    extendlet-check-period
                //    theme-provider-class
                //    theme-registry-class    /// since 6.5.2
                //    theme-resolver-class    /// since 6.5.2
                //    theme-uri
                //    theme-uri-handler-class    /// since 9.6.0
                //    repeat-uuid
                parseDesktopConfig(config, el);
                parseClientConfig(config, el); //backward compatible with 2.4

            } else if ("client-config".equals(elnm)) { //since 3.0.0
                //client-config
                //  keep-across-visits
                //  processing-prompt-delay
                //    error-reload
                //    tooltip-delay
                //  resend-delay
                //  debug-js
                //  enable-source-map
                //  send-client-errors
                //  auto-resend-timeout
                parseClientConfig(config, el);

            } else if ("session-config".equals(elnm)) {
                //session-config
                //    session-timeout
                //    max-desktops-per-session
                //  max-requests-per-session
                //    max-pushes-per-session
                //  timer-keep-alive
                //    timeout-uri
                //  automatic-timeout
                Integer v = parseInteger(el, "session-timeout", ANY_VALUE);
                if (v != null)
                    config.setSessionMaxInactiveInterval(v.intValue());

                v = parseInteger(el, "max-desktops-per-session", ANY_VALUE);
                if (v != null)
                    config.setSessionMaxDesktops(v.intValue());

                v = parseInteger(el, "max-requests-per-session", ANY_VALUE);
                if (v != null)
                    config.setSessionMaxRequests(v.intValue());

                v = parseInteger(el, "max-pushes-per-session", ANY_VALUE);
                if (v != null)
                    config.setSessionMaxPushes(v.intValue());

                String s = el.getElementValue("timer-keep-alive", true);
                if (s != null)
                    config.setTimerKeepAlive("true".equals(s));

                parseTimeoutURI(config, el);
            } else if ("language-config".equals(elnm)) {
                //language-config
                //    addon-uri
                parseLangConfig(locator, el);
            } else if ("language-mapping".equals(elnm)) {
                //language-mapping
                //    language-name/extension
                DefinitionLoaders.addExtension(IDOMs.getRequiredElementValue(el, "extension"),
                        IDOMs.getRequiredElementValue(el, "language-name"));
                //Note: we don't add it to LanguageDefinition now
                //since addon-uri might be specified later
                //(so we cannot load definitions now)
            } else if ("system-config".equals(elnm)) {
                //system-config
                //  disable-event-thread
                //    disable-zscript
                //    max-spare-threads
                //  max-suspended-threads
                //    event-time-warning
                //  max-upload-size
                //  upload-charset
                //  upload-charset-finder-class
                //  max-process-time
                //    response-charset
                //  cache-provider-class
                //  ui-factory-class
                //  failover-manager-class
                //    engine-class
                //    id-generator-class
                //  web-app-class
                //    method-cache-class
                //    url-encoder-class
                //    au-writer-class
                //    au-decoder-class
                parseSystemConfig(config, el);
            } else if ("xel-config".equals(elnm)) {
                //xel-config
                //    evaluator-class
                Class<? extends ExpressionFactory> cls = parseClass(el, "evaluator-class", ExpressionFactory.class);
                if (cls != null)
                    config.setExpressionFactoryClass(cls);
            } else if ("zscript-config".equals(elnm)) {
                //zscript-config
                Interpreters.add(el);
                //Note: zscript-config is applied to the whole system, not just langdef
            } else if ("device-config".equals(elnm)) {
                //device-config
                Devices.add(el);
                //Note: device-config is applied to the whole system, not just langdef
                parseTimeoutURI(config, el);
            } else if ("error-page".equals(elnm)) {
                //error-page
                final Class cls = parseClass(el, "exception-type", Throwable.class, true);
                final String loc = IDOMs.getRequiredElementValue(el, "location");
                String deviceType = el.getElementValue("device-type", true);
                if (deviceType == null)
                    deviceType = "ajax";
                else if (deviceType.length() == 0)
                    log.error("device-type not specified at " + el.getLocator());

                config.addErrorPage(deviceType, cls, loc);
            } else if ("preference".equals(elnm)) {
                parsePreference(config, el);
            } else if ("library-property".equals(elnm)) {
                parseLibProperty(el);
            } else if ("system-property".equals(elnm)) {
                parseSysProperty(el);
            } else {
                if (_parsers != null)
                    for (org.zkoss.zk.ui.util.ConfigParser parser : _parsers) {
                        if (parser.parse(config, el))
                            continue l_out;
                    }
                log.error("Unknown element: " + elnm + ", at " + el.getLocator());
            }
        }
    }

    private static void parseProperties(Element root) {
        for (Iterator it = root.getElements("library-property").iterator(); it.hasNext();) {
            parseLibProperty((Element) it.next());
        }
        for (Iterator it = root.getElements("system-property").iterator(); it.hasNext();) {
            parseSysProperty((Element) it.next());
        }
    }

    /**
     * Internal use only
     */
    public static void parseLibProperty(Element el) {
        final String nm = IDOMs.getRequiredElementValue(el, "name");
        Element valueElmn = el.getElement("value");
        Element appendableElmn = el.getElement("appendable");
        boolean appendable = appendableElmn != null ? "true".equals(appendableElmn.getText(true)) : false;
        Element listElmn = el.getElement("list");
        if (valueElmn != null && listElmn != null)
            throw new IllegalSyntaxException("You should not use <value> and <list> in <library-property> at the same time");
        else if (listElmn != null) {
            List<Element> valElements = listElmn.getElements("value");
            if (valElements == null || valElements.isEmpty())
                throw new IllegalSyntaxException(MCommon.XML_ELEMENT_REQUIRED, new Object[] {"value", el.getLocator()});
            List<String> values = new LinkedList<String>();
            for (Element valElmn : valElements) {
                String val = valElmn.getText(true);
                if (val != null & val.length() != 0)
                    values.add(val);
            }
            if (appendable)
                Library.addProperties(nm, values);
            else
                Library.setProperties(nm, values);
        } else if (valueElmn != null) {
            String val = valueElmn.getText(true);
            if (appendable)
                Library.addProperty(nm, val);
            else
                Library.setProperty(nm, val);
        } else {
            throw new IllegalSyntaxException(MCommon.XML_ELEMENT_REQUIRED, new Object[] {"<value> or <list>", el.getLocator()});
        }
    }

    private static void parseSysProperty(Element el) {
        final String nm = IDOMs.getRequiredElementValue(el, "name");
        final String val = IDOMs.getRequiredElementValue(el, "value");
        System.setProperty(nm, val);
    }

    private static void parsePreferences(Configuration config, Element root) {
        for (Iterator it = root.getElements("preference").iterator(); it.hasNext();) {
            parsePreference(config, (Element) it.next());
        }
    }

    private static void parsePreference(Configuration config, Element el) {
        final String nm = IDOMs.getRequiredElementValue(el, "name");
        final String val = IDOMs.getRequiredElementValue(el, "value");
        config.setPreference(nm, val);
    }

    /** Parses timeout-uri an other info. */
    private static void parseTimeoutURI(Configuration config, Element conf) throws Exception {
        String deviceType = conf.getElementValue("device-type", true);
        String s = conf.getElementValue("timeout-uri", true);
        if (s != null)
            config.setTimeoutURI(deviceType, s, URIInfo.SEND_REDIRECT);

        s = conf.getElementValue("timeout-message", true);
        if (s != null)
            config.setTimeoutMessage(deviceType, s);

        s = conf.getElementValue("automatic-timeout", true);
        if (s != null)
            config.setAutomaticTimeout(deviceType, !"false".equals(s));
    }

    /** Parses desktop-config. */
    private static void parseDesktopConfig(Configuration config, Element conf) throws Exception {
        //theme-uri
        for (Iterator<Element> it = conf.getElements("theme-uri").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            final String uri = el.getText(true);
            if (uri.length() != 0)
                config.addThemeURI(uri);
        }

        //disable-theme-uri
        for (Iterator<Element> it = conf.getElements("disable-theme-uri").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            final String uri = el.getText(true);
            if (uri.length() != 0)
                config.addDisabledThemeURI(uri);
        }

        // ZK-1671
        Class<?> cls = null;
        //theme-provider-class
        if (!config.isCustomThemeProvider()) {
            cls = parseClass(conf, "theme-provider-class", ThemeProvider.class);
            if (cls != null) {
                if (!cls.getName().startsWith("org.zkoss."))
                    config.setCustomThemeProvider(true);
                if (log.isDebugEnabled())
                    log.debug("ThemeProvider: " + cls.getName());
                config.setThemeProvider((ThemeProvider) cls.newInstance());
            }
        }

        //theme-registry-class
        //since 6.5.2
        if (!config.isCustomThemeRegistry()) {
            cls = parseClass(conf, "theme-registry-class", ThemeRegistry.class);
            if (cls != null) {
                if (!cls.getName().startsWith("org.zkoss."))
                    config.setCustomThemeRegistry(true);
                if (log.isDebugEnabled())
                    log.debug("ThemeRegistry: " + cls.getName());
                ThemeFns.setThemeRegistry((ThemeRegistry) cls.newInstance());
            }
        }

        //theme-resolver-class
        //since 6.5.2
        if (!config.isCustomThemeResolver()) {
            cls = parseClass(conf, "theme-resolver-class", ThemeResolver.class);
            if (cls != null) {
                if (!cls.getName().startsWith("org.zkoss."))
                    config.setCustomThemeResolver(true);
                if (log.isDebugEnabled())
                    log.debug("ThemeResolver: " + cls.getName());
                ThemeFns.setThemeResolver((ThemeResolver) cls.newInstance());
            }
        }

        //theme-uri-handler-class
        //since 9.6.0
        cls = parseClass(conf, "theme-uri-handler-class", ThemeURIHandler.class);
        if (cls != null) {
            if (log.isDebugEnabled())
                log.debug("ThemeURIHandler: " + cls.getName());
            config.addThemeURIHandler((ThemeURIHandler) cls.newInstance());
        }

        //desktop-timeout
        Integer v = parseInteger(conf, "desktop-timeout", ANY_VALUE);
        if (v != null)
            config.setDesktopMaxInactiveInterval(v.intValue());

        //file-check-period
        v = parseInteger(conf, "file-check-period", POSITIVE_ONLY);
        if (v != null)
            Library.setProperty("org.zkoss.util.resource.checkPeriod", v.toString());
        //library-wide property

        //extendlet-check-period
        v = parseInteger(conf, "extendlet-check-period", POSITIVE_ONLY);
        if (v != null)
            Library.setProperty("org.zkoss.util.resource.extendlet.checkPeriod", v.toString());
        //library-wide property
    }

    /** Parses client-config. */
    @SuppressWarnings("deprecation")
    private static void parseSystemConfig(Configuration config, Element el) throws Exception {
        String s = el.getElementValue("disable-event-thread", true);
        if (s != null) {
            final boolean enable = "false".equals(s);
            if (!enable)
                log.info("The event processing thread is disabled");
            config.enableEventThread(enable);
        }
        s = el.getElementValue("disable-zscript", true);
        if (s != null)
            config.enableZScript(!"true".equals(s));

        Integer v = parseInteger(el, "max-spare-threads", ANY_VALUE);
        if (v != null)
            config.setMaxSpareThreads(v.intValue());

        v = parseInteger(el, "max-suspended-threads", ANY_VALUE);
        if (v != null)
            config.setMaxSuspendedThreads(v.intValue());

        v = parseInteger(el, "event-time-warning", ANY_VALUE);
        if (v != null)
            config.setEventTimeWarning(v.intValue());

        v = parseInteger(el, "max-upload-size", ANY_VALUE);
        if (v != null)
            config.setMaxUploadSize(v.intValue());

        v = parseInteger(el, "file-size-threshold", ANY_VALUE);
        if (v != null)
            config.setFileSizeThreshold(v.intValue());

        v = parseInteger(el, "max-process-time", POSITIVE_ONLY);
        if (v != null)
            config.setMaxProcessTime(v.intValue());

        s = el.getElementValue("upload-charset", true);
        if (s != null)
            config.setUploadCharset(s);

        s = el.getElementValue("response-charset", true);
        if (s != null)
            config.setResponseCharset(s);

        s = el.getElementValue("crawlable", true);
        if (s != null)
            config.setCrawlable(!"false".equals(s));

        // ZK-3105
        s = el.getElementValue("file-repository", true);
        if (s != null)
            config.setFileRepository(s);

        //bug B50-3316543
        for (Iterator it = el.getElements("label-location").iterator(); it.hasNext();) {
            final Element elinner = (Element) it.next();
            final String path = elinner.getText(true);
            if (!Strings.isEmpty(path))
                config.addLabelLocation(path);
        }

        Class cls = parseClass(el, "upload-charset-finder-class", CharsetFinder.class);
        if (cls != null)
            config.setUploadCharsetFinder((CharsetFinder) cls.newInstance());

        cls = parseClass(el, "cache-provider-class", DesktopCacheProvider.class);
        if (cls != null)
            config.setDesktopCacheProviderClass(cls);

        cls = parseClass(el, "ui-factory-class", UiFactory.class);
        if (cls != null)
            config.setUiFactoryClass(cls);

        cls = parseClass(el, "failover-manager-class", FailoverManager.class);
        if (cls != null)
            config.setFailoverManagerClass(cls);

        cls = parseClass(el, "engine-class", UiEngine.class);
        if (cls != null)
            config.setUiEngineClass(cls);

        cls = parseClass(el, "id-generator-class", IdGenerator.class);
        if (cls != null)
            config.setIdGeneratorClass(cls);

        cls = parseClass(el, "session-cache-class", SessionCache.class);
        if (cls != null)
            config.setSessionCacheClass(cls);

        // ZK-3105
        cls = parseClass(el, "file-item-factory-class", DiskFileItemFactory.class);
        if (cls != null)
            config.setFileItemFactoryClass(cls);

        cls = parseClass(el, "au-decoder-class", AuDecoder.class);
        if (cls != null)
            config.setAuDecoderClass(cls);

        cls = parseClass(el, "web-app-class", WebApp.class);
        if (cls != null)
            config.setWebAppClass(cls);

        cls = parseClass(el, "web-app-factory-class", WebAppFactory.class);
        if (cls != null)
            config.setWebAppFactoryClass(cls);

        cls = parseClass(el, "method-cache-class", Cache.class);
        if (cls != null)
            ComponentsCtrl.setEventMethodCache((Cache) cls.newInstance());

        cls = parseClass(el, "au-writer-class", AuWriter.class);
        if (cls != null)
            AuWriters.setImplementationClass(cls);
    }

    /** Parses client-config. */
    private static void parseClientConfig(Configuration config, Element conf) {
        Integer v = parseInteger(conf, "processing-prompt-delay", POSITIVE_ONLY);
        if (v != null)
            config.setProcessingPromptDelay(v.intValue());

        v = parseInteger(conf, "tooltip-delay", POSITIVE_ONLY);
        if (v != null)
            config.setTooltipDelay(v.intValue());

        v = parseInteger(conf, "auto-resend-timeout", POSITIVE_ONLY);
        if (v != null)
            config.setAutoResendTimeout(v.intValue());

        String s = conf.getElementValue("keep-across-visits", true);
        if (s != null)
            config.setKeepDesktopAcrossVisits(!"false".equals(s));

        s = conf.getElementValue("debug-js", true);
        if (s != null)
            config.setDebugJS(!"false".equals(s));

        s = conf.getElementValue("enable-source-map", true);
        if (s != null)
            config.enableSourceMap(!"false".equals(s));

        //F100-ZK-5135: add new config for whether to send client errors to the server
        s = conf.getElementValue("send-client-errors", true);
        if (s != null)
            config.setSendClientErrors(!"false".equals(s));

        //F70-ZK-2495: add new config to customize crash script
        s = conf.getElementValue("init-crash-script", true);
        if (s != null)
            config.setInitCrashScript(s);

        //F70-ZK-2495: add new config to customize timeout
        v = parseInteger(conf, "init-crash-timeout", NON_NEGATIVE);
        if (v != null)
            config.setInitCrashTimeout(v.intValue());

        //ZK-4179: add new config to disable ZK history API
        s = conf.getElementValue("enable-history-state", true);
        if (s != null)
            config.enableHistoryState(Boolean.parseBoolean(s));

        //client (JS) packages
        for (Iterator it = conf.getElements("package").iterator(); it.hasNext();) {
            config.addClientPackage(IDOMs.getRequiredElementValue((Element) it.next(), "package-name"));
        }

        //client data-attr handlers
        for (Iterator<Element> it = conf.getElements("data-handler").iterator(); it.hasNext();) {
            //config.addClientPackage(IDOMs.getRequiredElementValue((Element)it.next(), "package-name"));
            final Element el = it.next();
            String dataName = IDOMs.getRequiredElementValue(el, "name");
            List<Element> elements = el.getElements("script");
            List<Pair<String, String>> scripts = null;
            if (!elements.isEmpty()) {
                scripts = new LinkedList<Pair<String, String>>();
                for (Iterator<Element> itt = elements.iterator(); itt.hasNext();) {
                    Element e = itt.next();
                    scripts.add(new Pair<String, String>(e.getAttribute("src"), e.getText(true)));
                }
            }
            if (scripts == null)
                throw new IllegalSyntaxException(MCommon.XML_ELEMENT_REQUIRED,
                        new Object[] { "script", el.getLocator() });

            boolean override = Boolean.parseBoolean(el.getElementValue("override", true));

            elements = el.getElements("link");
            List<Map<String, String>> links = null;
            if (!elements.isEmpty()) {
                links = new LinkedList<Map<String, String>>();
                for (Iterator<Element> itt = elements.iterator(); itt.hasNext();) {
                    Element e = itt.next();
                    List<Attribute> attrs = e.getAttributeItems();
                    if (attrs == null || attrs.isEmpty())
                        continue;
                    Map<String, String> attrMap = new LinkedHashMap<String, String>();
                    for (Attribute a : attrs)
                        attrMap.put(a.getName(), a.getValue());
                    links.add(attrMap);
                }
            }
            config.addDataHandler(new DataHandlerInfo(dataName, scripts, override, links));
        }
        //error-reload
        for (Iterator it = conf.getElements("error-reload").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();

            String deviceType = el.getElementValue("device-type", true);
            String connType = el.getElementValue("connection-type", true);
            v = parseInteger(el, "error-code", NON_NEGATIVE);
            if (v == null)
                throw new UiException(message("error-code is required", el));
            String uri = IDOMs.getRequiredElementValue(el, "reload-uri");
            if ("false".equals(uri))
                uri = null;

            config.setClientErrorReload(deviceType, v.intValue(), uri, connType);
        }
    }

    private static String message(String message, org.zkoss.idom.Item el) {
        return org.zkoss.xml.Locators.format(message, el != null ? el.getLocator() : null);
    }

    /** Parse language-config */
    private static void parseLangConfigs(Locator locator, Element root) {
        for (Iterator it = root.getElements("language-config").iterator(); it.hasNext();) {
            parseLangConfig(locator, (Element) it.next());
        }
    }

    /** Parse language-config/addon-uri. */
    private static void parseLangConfig(Locator locator, Element conf) {
        for (Iterator it = conf.getElements("addon-uri").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            final String path = el.getText(true);

            final URL url = locator.getResource(path);
            if (url == null)
                log.error("File not found: " + path + ", at " + el.getLocator());
            else
                DefinitionLoaders.addAddon(locator, url);
        }
        for (Iterator it = conf.getElements("language-uri").iterator(); it.hasNext();) {
            final Element el = (Element) it.next();
            final String path = el.getText(true);

            final URL url = locator.getResource(path);
            if (url == null)
                log.error("File not found: " + path + ", at " + el.getLocator());
            else
                DefinitionLoaders.addLanguage(locator, url);
        }
    }

    /** Parse a class, if specified, whether it implements cls.
     */
    private static <T> Class<T> parseClass(Element el, String elnm, Class cls) {
        return parseClass(el, elnm, cls, false);
    }

    private static <T> Class<T> parseClass(Element el, String elnm, Class<?> cls, boolean required) {
        //Note: we throw exception rather than warning to make sure
        //the developer correct it
        final String clsnm = el.getElementValue(elnm, true);
        if (clsnm != null && clsnm.length() != 0) {
            try {
                final Class<?> klass = Classes.forNameByThread(clsnm);
                if (cls != null && !cls.isAssignableFrom(klass)) {
                    String msg = message(clsnm + " must implement " + cls.getName(), el);
                    if (required)
                        throw new UiException(msg);
                    log.error(msg);
                    return null;
                }
                //                if (log.debuggable()) log.debug("Using "+clsnm+" for "+cls);
                return cast(klass);
            } catch (Throwable ex) {
                String msg = ex instanceof ClassNotFoundException ? clsnm + " not found" : "Unable to load " + clsnm;
                msg = message(msg, el);
                if (required)
                    throw new UiException(msg, ex);
                log.error(msg);
                return null;
            }
        } else if (required)
            throw new UiException(message(elnm + " required", el));
        return null;
    }

    /** Configures an integer.
     * @param flag one of POSTIVE_ONLY, NON_NEGATIVE and ANY_VALUE.
     */
    private static Integer parseInteger(Element el, String subnm, int flag) throws UiException {
        //Note: we throw exception rather than warning to make sure
        //the developer correct it
        String val = el.getElementValue(subnm, true);
        if (val != null && val.length() > 0) {
            try {
                final int v = Integer.parseInt(val);
                if ((flag == POSITIVE_ONLY && v <= 0) || (flag == NON_NEGATIVE && v < 0))
                    throw new UiException(message(
                            "The " + subnm + " element must be a "
                                    + (flag == POSITIVE_ONLY ? "positive" : "non-negative") + " number, not " + val,
                            el));
                return new Integer(v);
            } catch (NumberFormatException ex) { //eat
                throw new UiException(message("The " + subnm + " element must be a number, not " + val, el));
            }
        }
        return null;
    }

    private static final int POSITIVE_ONLY = 2;
    private static final int NON_NEGATIVE = 1;
    private static final int ANY_VALUE = 0;

    //F80 - store subtree's binder annotation count
    private static void parseBinderConfig(Configuration config, Element conf) {
        Element binderConf = conf.getElement("binder-config");
        if (binderConf != null) {
            config.setBinderInitAttribute(binderConf.getElement("binder-init-attribute").getText());
            List<Element> values = binderConf.getElement("binding-annotations").getElement("list").getElements();
            Set<String> annots = new HashSet<String>();
            for (Element val : values) {
                annots.add("@" + val.getText() + "(");
            }
            config.setBinderAnnotations(annots);
        }
    }
}