zk/src/main/java/org/zkoss/zk/ui/metainfo/Parser.java

Summary

Maintainability
F
1 mo
Test Coverage
/* Parser.java

    Purpose:
        
    Description:
        
    History:
        Tue May 31 14:26:18     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui.metainfo;

import java.io.File;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;

import org.zkoss.idom.Attribute;
import org.zkoss.idom.CData;
import org.zkoss.idom.Comment;
import org.zkoss.idom.Document;
import org.zkoss.idom.Element;
import org.zkoss.idom.Item;
import org.zkoss.idom.Namespace;
import org.zkoss.idom.ProcessingInstruction;
import org.zkoss.idom.Text;
import org.zkoss.lang.ClassResolver;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.PotentialDeadLockException;
import org.zkoss.util.CollectionsX;
import org.zkoss.util.resource.Locator;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.xel.taglib.Taglib;
import org.zkoss.xel.util.Evaluators;
import org.zkoss.xel.util.MethodFunction;
import org.zkoss.xml.Locators;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.Native;
import org.zkoss.zk.ui.impl.RequestInfoImpl;
import org.zkoss.zk.ui.impl.ZScriptInitiator;
import org.zkoss.zk.ui.metainfo.impl.AnnotationHelper;
import org.zkoss.zk.ui.metainfo.impl.ComponentDefinitionImpl;
import org.zkoss.zk.ui.metainfo.impl.ShadowDefinitionImpl;
import org.zkoss.zk.ui.sys.RequestInfo;
import org.zkoss.zk.ui.sys.UiFactory;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.ConditionImpl;
import org.zkoss.zk.ui.util.Configuration;

/**
 * Used to parse the ZUL file
 * @author tomyeh
 */
public class Parser {
    private static final Logger log = LoggerFactory.getLogger(Parser.class);

    private final WebApp _wapp;
    private final Locator _locator;
    private final List<NamespaceParser> _nsParsers;

    /** Constructor.
     *
     * @param locator the locator used to locate taglib and other resources.
     * If null, wapp is assumed ({@link WebApp} is also assumed).
     */
    public Parser(WebApp wapp, Locator locator) {
        if (wapp == null)
            throw new IllegalArgumentException("null");
        _wapp = wapp;
        _nsParsers = wapp.getConfiguration().getNamespaceParsers();

        // Higher is the first
        Collections.sort(_nsParsers, new Comparator<NamespaceParser>() {
            public int compare(NamespaceParser o1, NamespaceParser o2) {
                int op1 = o1.getPriority();
                int op2 = o2.getPriority();
                return (op1 < op2) ? 1 : (op1 > op2) ? -1 : 0;
            }
        });
        _locator = locator != null ? locator : wapp;
    }

    /** Parses the specified file.
     *
     * @param path the request path.
     * It is used as {@link org.zkoss.zk.ui.Page#getRequestPath}, or null
     * if not available.
     */
    public PageDefinition parse(File file, String path) throws Exception {
        //if (log.isDebugEnabled()) log.debug("Parsing "+file);
        String extension = Servlets.getExtension(file.getName());
        final PageDefinition pgdef = parse(TreeBuilderFactory.makeBuilder(extension).parse(file), extension);
        pgdef.setRequestPath(path);
        return pgdef;
    }

    /** Parses the specified URL.
     *
     * @param path the request path.
     * It is used as {@link org.zkoss.zk.ui.Page#getRequestPath}, or null
     * if not available.
     */
    public PageDefinition parse(URL url, String path) throws Exception {
        //if (log.isDebugEnabled()) log.debug("Parsing "+url);
        String extension = Servlets.getExtension(url.toExternalForm());
        final PageDefinition pgdef = parse(TreeBuilderFactory.makeBuilder(extension).parse(url), extension);
        pgdef.setRequestPath(path);
        return pgdef;
    }

    /** Parses from the specified reader.
     *
     * @param extension the default extension if doc (of reader) doesn't specify
     * an language. Ignored if null.
     * If extension is null and the content doesn't specify a language,
     * the language called "xul/html" is assumed.
     */
    public PageDefinition parse(Reader reader, String extension) throws Exception {
        //if (log.isDebugEnabled()) log.debug("Parsing "+reader);
        return parse(TreeBuilderFactory.makeBuilder(extension).parse(reader), extension);
    }

    /** Parse the raw content directly from a DOM tree.
     *
     * @param extension the default extension if doc doesn't specify
     * an language. Ignored if null.
     * If extension is null and the content doesn't specify a language,
     * the language called "xul/html" is assumed.
     */
    public PageDefinition parse(Document doc, String extension) throws Exception {
        //1. parse the page and import directive if any
        final List<ProcessingInstruction> pis = new LinkedList<ProcessingInstruction>();
        final List<String[]> imports = new LinkedList<String[]>();
        final List<String> impclses = new LinkedList<String>();
        String lang = null;
        for (Object o : doc.getChildren()) {
            if (!(o instanceof ProcessingInstruction))
                continue;

            final ProcessingInstruction pi = (ProcessingInstruction) o;
            final String target = pi.getTarget();
            if ("page".equals(target)) {
                //we handle only the language attribute here
                final Map<String, String> params = pi.parseData();
                final String l = params.remove("language");
                if (l != null) {
                    noEL("language", l, pi);
                    lang = l;
                }

                if (!params.isEmpty())
                    pis.add(pi); //process it later
            } else if ("import".equals(target)) { //import
                final Map<String, String> params = pi.parseData();
                final String src = params.remove("src");
                final String dirs = params.remove("directives");
                final String cls = params.remove("class");
                if (src != null) {
                    noELnorEmpty("src", src, pi);
                    noEL("directives", dirs, pi);
                    imports.add(new String[] { src, dirs });
                }
                if (cls != null) {
                    noELnorEmpty("class", cls, pi);
                    impclses.add(cls);
                }
                for (Map.Entry<String, String> me : params.entrySet()) {
                    final String nm = me.getKey();
                    final String val = me.getValue();
                    if (val == null) {
                        noELnorEmpty(nm, nm, pi);
                        impclses.add(nm);
                    } else {
                        log.warn(message("Ignored unknown attribute for import: " + nm, pi));
                    }
                }
            } else {
                pis.add(pi);
            }
        }

        //2. Create PageDefinition
        final LanguageDefinition langdef = extension != null && lang == null
                ? LanguageDefinition.getByExtension(extension) : LanguageDefinition.lookup(lang);
        final PageDefinition pgdef = new PageDefinition(langdef, getLocator());

        //3a. resolve imports
        if (!imports.isEmpty()) {
            final RequestInfo ri = new RequestInfoImpl(_wapp, null, null, null, getLocator());
            final UiFactory uf = ((WebAppCtrl) _wapp).getUiFactory();
            for (final String[] imprt : imports) {
                final String path = imprt[0], dirs = imprt[1];
                try {
                    final PageDefinition pd = uf.getPageDefinition(ri, path);
                    if (pd == null)
                        throw new UiException("The imported page not found: " + path);
                    pgdef.imports(pd, parseToArray(dirs));
                } catch (PotentialDeadLockException ex) {
                    throw new UiException("Recursive import not allowed: " + path, ex);
                }
            }
        }
        //3b. resolve imported classes
        for (String impcls : impclses)
            pgdef.addImportedClass(impcls);

        //4. Processing the rest of processing instructions at the top level
        for (ProcessingInstruction pi : pis)
            parse(pgdef, pi);

        //5. Processing from the root element
        final Element root = doc.getRootElement();
        if (root != null)
            parseItem(pgdef, pgdef, root, new AnnotationHelper(), false, ParsingState.ROOT); //ZK-2632: Parser support disorder template tag

        return pgdef;
    }

    /** Parse processing instruction.
     */
    private void parse(PageDefinition pgdef, ProcessingInstruction pi) throws Exception {
        final String target = pi.getTarget();
        final Map<String, String> params = pi.parseData();
        if ("page".equals(target)) {
            parsePageDirective(pgdef, pi, params);
        } else if ("init".equals(target)) {
            parseInitDirective(pgdef, pi, params);
        } else if ("variable-resolver".equals(target) || "function-mapper".equals(target)) {
            final String clsnm = params.remove("class");
            if (isEmpty(clsnm))
                throw new UiException(message("The class attribute is required", pi));

            final Map<String, String> args = new LinkedHashMap<String, String>(params);
            if ("variable-resolver".equals(target))
                pgdef.addVariableResolverInfo(new VariableResolverInfo(clsnm, args));
            else
                pgdef.addFunctionMapperInfo(new FunctionMapperInfo(clsnm, args));
        } else if ("component".equals(target)) { //declare a component
            parseComponentDirective(pgdef, pi, params);
        } else if ("taglib".equals(target)) {
            final String uri = params.remove("uri");
            final String prefix = params.remove("prefix");
            if (!params.isEmpty())
                log.warn(message("Ignored unknown attributes: " + params.keySet(), pi));
            if (uri == null || prefix == null)
                throw new UiException(message("Both uri and prefix attribute are required", pi));
            //if (log.isDebugEnabled()) log.debug("taglib: prefix="+prefix+" uri="+uri);
            noEL("prefix", prefix, pi);
            noEL("uri", uri, pi); //not support EL (kind of chicken-egg issue)
            pgdef.addTaglib(new Taglib(prefix, toAbsoluteURI(uri)));
        } else if ("evaluator".equals(target)) {
            parseEvaluatorDirective(pgdef, pi, params);
        } else if ("xel-method".equals(target)) {
            parseXelMethod(pgdef, pi, params);
        } else if ("link".equals(target) || "meta".equals(target) || "script".equals(target)
                || "style".equals(target)) { //declare a header element
            pgdef.addHeaderInfo(new HeaderInfo(target, params,
                    ConditionImpl.getInstance(params.remove("if"), params.remove("unless"))));
        } else if ("header".equals(target)) { //declare a response header
            pgdef.addResponseHeaderInfo(new ResponseHeaderInfo(params.remove("name"), params.remove("value"),
                    params.remove("append"), ConditionImpl.getInstance(params.remove("if"), params.remove("unless"))));
        } else if ("root-attributes".equals(target)) {
            for (Map.Entry<String, String> stringStringEntry : pi.parseData().entrySet()) {
                final Map.Entry me = (Map.Entry) stringStringEntry;
                pgdef.setRootAttribute((String) me.getKey(), (String) me.getValue());
            }
        } else if ("forward".equals(target)) { //forward
            final String uri = params.remove("uri");
            final String ifc = params.remove("if");
            final String unless = params.remove("unless");
            if (!params.isEmpty())
                log.warn(message("Ignored unknown attributes: " + params.keySet(), pi));
            noEmpty("uri", uri, pi);
            pgdef.addForwardInfo(new ForwardInfo(uri, ConditionImpl.getInstance(ifc, unless)));
        } else if ("import".equals(target)) { //import
            throw new UiException(message("The import directive can be used only at the top level", pi));
        } else if ("template".equals(target)) { //ZK-2623: page scope template
            final String name = params.remove("name");
            final String src = params.remove("src");
            if (name != null && name.length() != 0 && src != null && src.length() != 0)
                pgdef.addTemplateInfo(new TemplateInfo(null, name, src, params, null));
        } else {
            log.warn(message("Unknown processing instruction: " + target, pi));
        }
    }

    /** Parses a list of string separated by comma, into a String array.
     */
    private static String[] parseToArray(String s) {
        if (s == null)
            return null;
        Collection<String> ims = CollectionsX.parse(null, s, ',', false); //NO EL
        return ims.toArray(new String[ims.size()]);
    }

    //-- derive to override --//
    /** returns locator for locating resources. */
    public Locator getLocator() {
        return _locator;
    }

    /** Process the init directive. */
    private void parseInitDirective(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
            throws Exception {
        final String clsnm = params.remove("class");
        final String zsrc = params.remove("zscript");

        final Map<String, String> args = new LinkedHashMap<String, String>(params);
        if (clsnm == null) {
            if (zsrc == null)
                throw new UiException(message("Either the class or zscript attribute must be specified", pi));

            checkZScriptEnabled(pi.getLocator());
            ZScript zs = null;
            final String zslang = pgdef.getZScriptLanguage();
            if (!zsrc.contains("${")) {
                final URL url = getLocator().getResource(zsrc);
                if (url != null)
                    zs = new ZScript(zslang, url);
                //Bug 2929887: defer the error message since it might not be required
            }
            if (zs == null)
                zs = new ZScript(pgdef.getEvaluatorRef(), zslang, zsrc, getLocator());

            pgdef.addInitiatorInfo(new InitiatorInfo(new ZScriptInitiator(zs), args));
        } else {
            if (zsrc != null)
                throw new UiException(message("You cannot specify both class and zscript", pi));

            pgdef.addInitiatorInfo(new InitiatorInfo(clsnm, args));
        }
    }

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

    private static String message(String message, org.zkoss.xml.Locator loc) {
        return org.zkoss.xml.Locators.format(message, loc);
    }

    private static org.zkoss.util.resource.Location location(Item el) {
        return org.zkoss.xml.Locators.toLocation(el != null ? el.getLocator() : null);
    }

    private void checkZScriptEnabled(Element el) {
        checkZScriptEnabled(el.getLocator());
    }

    private void checkZScriptEnabled(org.zkoss.xml.Locator loc) {
        if (!_wapp.getConfiguration().isZScriptEnabled())
            throw new UiException(message("zscript is not allowed since <disable-zscript> is configured", loc));
    }

    /** Process the page directive. */
    private static void parsePageDirective(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
            throws Exception {
        for (Map.Entry<String, String> me : pi.parseData().entrySet()) {
            final String nm = me.getKey();
            final String val = me.getValue();
            if ("language".equals(nm)) {
                if (!(pi.getParent() instanceof Document))
                    log.warn(
                            message("Ignored language attribute since the page directive is not at the top level", pi));
            } else if ("title".equals(nm)) {
                pgdef.setTitle(val);
            } else if ("style".equals(nm)) {
                pgdef.setStyle(val);
            } else if ("viewport".equals(nm)) {
                pgdef.setViewport(val);
            } else if ("id".equals(nm)) {
                pgdef.setId(val);
            } else if ("widgetClass".equals(nm)) {
                pgdef.setWidgetClass(val);
            } else if ("zscriptLanguage".equals(nm) || "zscript-language".equals(nm)) { //backward compatible with 2.4.x
                noELnorEmpty("zscriptLanguage", val, pi);
                pgdef.setZScriptLanguage(val);
            } else if ("cacheable".equals(nm)) {
                noELnorEmpty("cacheable", val, pi);
                pgdef.setCacheable(Boolean.valueOf("true".equals(val)));
            } else if ("automaticTimeout".equals(nm)) {
                noELnorEmpty("automaticTimeout", val, pi);
                pgdef.setAutomaticTimeout(Boolean.valueOf("true".equals(val)));
            } else if ("contentType".equals(nm)) {
                noEmpty("contentType", val, pi);
                pgdef.setContentType(val);
            } else if ("docType".equals(nm)) {
                pgdef.setDocType(isEmpty(val) ? "" : "<!DOCTYPE " + val + '>');
            } else if ("xml".equals(nm)) {
                noEmpty("xml", val, pi);
                pgdef.setFirstLine("<?xml " + val + "?>");
            } else if ("complete".equals(nm)) {
                pgdef.setComplete("true".equals(val));
            } else {
                log.warn(message("Ignored unknown attribute: " + nm, pi));
            }
        }
    }

    /** Process the component directive. */
    private void parseComponentDirective(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
            throws Exception {
        final String name = params.remove("name");
        noELnorEmpty("name", name, pi);

        String macroURI = params.remove("macroURI");
        if (macroURI == null)
            macroURI = params.remove("macro-uri"); //backward compatible (2.4.x)
        final String extds = params.remove("extends");
        final String clsnm = params.remove("class");
        final String lang = params.remove("language");
        final LanguageDefinition langdef = lang != null ? LanguageDefinition.lookup(lang)
                : pgdef.getLanguageDefinition();
        ComponentDefinition compdef;
        String templateURI = params.remove("templateURI");
        if (templateURI != null) { // assume it is shadow like (<apply>)
            noEL("templateURI", templateURI, pi);
            //no EL because pagedef must be loaded to resolve
            //the implementing class before creating an instance of shadow

            compdef = langdef.getShadowDefinition(name, pgdef, toAbsoluteURI(templateURI));
            if (!isEmpty(clsnm)) {
                noEL("class", clsnm, pi);
                compdef.setImplementationClass(clsnm);
                //Resolve later since might defined in zscript
            }
        } else if (macroURI != null) {
            //if (log.finerable()) log.finer("macro component definition: "+name);

            final String inline = params.remove("inline");
            noEL("inline", inline, pi);
            noEL("macroURI", macroURI, pi);
            //no EL because pagedef must be loaded to resolve
            //the implementing class before creating an instance of macro

            final boolean bInline = "true".equals(inline);
            compdef = langdef.getMacroDefinition(name, toAbsoluteURI(macroURI), bInline, pgdef);
            if (!isEmpty(clsnm)) {
                if (bInline)
                    throw new UiException(message("class not allowed with inline macros", pi));
                noEL("class", clsnm, pi);
                compdef.setImplementationClass(clsnm);
                //Resolve later since might defined in zscript
            }
        } else {
            ComponentDefinition ref = null;
            if (extds != null) { //extends
                //if (log.finerable()) log.finer("Override component definition: "+name);

                noEL("extends", extds, pi);
                ref = langdef.getComponentDefinition(extds);
            } else {
                final Class cls = pgdef.getImportedClassResolver().resolveClass(clsnm);
                if (lang != null) {
                    ref = langdef.getComponentDefinitionIfAny(cls);
                } else {
                    ref = pgdef.getComponentDefinition(cls, true);
                    if (ref == null) //return null if not found
                        ref = Components.getDefinitionByDeviceType(langdef.getDeviceType(), cls);
                }
            }

            if (ref != null) {
                if (ref.isMacro())
                    throw new UiException(message("Unable to extend from a macro component", pi));

                compdef = ref.clone(null, name);
                if (!isEmpty(clsnm)) {
                    noEL("class", clsnm, pi);
                    compdef.setImplementationClass(clsnm);
                    //Resolve later since might defined in zscript
                }
            } else {
                //if (log.finerable()) log.finer("Add component definition: name="+name);

                noELnorEmpty("class", clsnm, pi);

                final ComponentDefinitionImpl cdi = new ComponentDefinitionImpl(null, pgdef, name,
                        (Class<? extends Component>) null);
                cdi.setCurrentDirectory(getLocator().getDirectory());
                //mold URI requires it
                compdef = cdi;
                compdef.setImplementationClass(clsnm);
                //Resolve later since might be defined in zscript
            }
        }

        String wgtnm = params.remove("widgetClass");
        if (wgtnm == null)
            wgtnm = params.remove("widget-class");
        if (wgtnm != null)
            compdef.setDefaultWidgetClass(wgtnm);

        pgdef.addComponentDefinition(compdef);

        Object o = params.remove("moldURI");
        if (o == null)
            o = params.remove("mold-uri");
        if (o != null)
            throw new UnsupportedOperationException(
                    message("moldURI not supported in 5.0. Use <?script?> or lang-addon.xml instead", pi));

        o = params.remove("cssURI");
        if (o != null)
            throw new UnsupportedOperationException(
                    message("cssURI not supported in 5.0. Use <?link?> or lang-addon.xml instead", pi));

        compdef.setApply(params.remove("apply"));

        for (Map.Entry<String, String> me : params.entrySet()) {
            compdef.addProperty(me.getKey(), me.getValue());
        }
    }

    /** Parse the evaluator directive. */
    private static void parseEvaluatorDirective(PageDefinition pgdef, ProcessingInstruction pi,
            Map<String, String> params) throws Exception {
        final String clsnm = params.remove("class");
        if (clsnm != null && clsnm.length() > 0) {
            noELnorEmpty("class", clsnm, pi);
            pgdef.setExpressionFactoryClass(pgdef.getImportedClassResolver().resolveClass(clsnm));
        } else { //name has the lower priority
            final String nm = params.remove("name");
            if (nm != null)
                pgdef.setExpressionFactoryClass(Evaluators.getEvaluatorClass(nm));
        }

        final String imports = params.remove("import");
        if (imports != null && imports.length() > 0) {
            Collection<String> ims = CollectionsX.parse(null, imports, ',', false); //No EL
            for (String im : ims) {
                final int k = im.indexOf('=');
                String nm = k > 0 ? im.substring(0, k).trim() : null;
                String cn = (k >= 0 ? im.substring(k + 1) : im).trim();

                if (cn.length() != 0) {
                    if (nm == null || nm.length() == 0) {
                        final int j = cn.lastIndexOf('.');
                        nm = j >= 0 ? cn.substring(j + 1) : cn;
                    }
                    pgdef.addExpressionImport(nm, Classes.forNameByThread(cn));
                    //evaluator's import does not support <?import class?>,
                    //since it looks strange.
                    //FUTURE: it is better to deprecate this attribute, and
                    //have FunctionMapperExt to depend on <?import class?>
                    //(however, it is worth since only MVEL/OGNL uses it)
                }
            }
        }
    }

    /** Parse the XEL method. */
    private static void parseXelMethod(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
            throws Exception {
        final String prefix = params.remove("prefix");
        noELnorEmpty("prefix", prefix, pi);
        final String nm = params.remove("name");
        noELnorEmpty("name", nm, pi);
        final String clsnm = params.remove("class");
        noELnorEmpty("class", clsnm, pi);
        final String sig = params.remove("signature");
        noELnorEmpty("signature", sig, pi);

        final Method mtd;
        try {
            final ClassResolver clsresolver = pgdef.getImportedClassResolver();
            final Class cls = clsresolver.resolveClass(clsnm);
            mtd = Classes.getMethodBySignature(cls, sig, null, clsresolver);
        } catch (ClassNotFoundException ex) {
            throw new UiException(message("Class not found: " + ex.getMessage(), pi));
        } catch (Exception ex) {
            throw new UiException(message("Method not found: " + sig + " in " + clsnm, pi));
        }
        if ((mtd.getModifiers() & Modifier.STATIC) == 0)
            throw new UiException(message("Not a static method: " + mtd, pi));

        pgdef.addXelMethod(prefix, nm, new MethodFunction(mtd));
    }

    private static void noELnorEmpty(String nm, String val, Item item) throws UiException {
        if (isEmpty(val))
            throw new UiException(message(nm + " cannot be empty", item));
        noEL(nm, val, item);
    }

    private static void noEL(String nm, String val, Item item) throws UiException {
        if (val != null && val.contains("${"))
            throw new UiException(message(nm + " does not support EL expressions", item));
    }

    /** Checks whether the value is an empty string.
     * Note: Like {@link #noEL}, it is OK to be null!!
     */
    private static void noEmpty(String nm, String val, Item item) throws UiException {
        if (val != null && val.length() == 0)
            throw new UiException(message(nm + " cannot be empty", item));
    }

    private String toAbsoluteURI(String uri) {
        if (uri != null && uri.length() > 0) {
            final char cc = uri.charAt(0);
            if (cc != '/' && cc != '~' && !Servlets.isUniversalURL(uri)) {
                final String dir = getLocator().getDirectory();
                if (dir != null && dir.length() > 0)
                    return dir.charAt(dir.length() - 1) == '/' ? dir + uri : dir + '/' + uri;
            }
        }
        return uri;
    }

    /** Parses the specified elements.
     * @param bNativeContent whether to consider the child element all native
     * It is true if a component definition with text-as is found
     */
    private void parseItems(final PageDefinition pgdef, final NodeInfo parent, Collection items,
            AnnotationHelper annHelper, boolean bNativeContent) throws Exception {
        LanguageDefinition parentlang = getLanguageDefinition(parent);
        if (parentlang == null)
            parentlang = pgdef.getLanguageDefinition();
        final boolean bZkSwitch = isZkSwitch(parent);

        ComponentInfo pi = null;
        String textAs = null;
        StringBuilder textAsBuffer = null;
        for (NodeInfo p = parent; p != null; p = p.getParent())
            if (p instanceof ComponentInfo) {
                pi = (ComponentInfo) p;
                textAs = pi.getTextAs();
                if (textAs != null && pi == parent) //only direct child
                    textAsBuffer = new StringBuilder();
                break; //found
            }
        final boolean isXHTML = "xhtml".equals(parentlang.getName());
        final boolean isAllBlankPreserved = !"false".equals(Library.getProperty("preserve-all-blank"));
        boolean breakLine = false;
        NativeInfo preNativeInfo = null; //check if brother is native

        //ZK-2632: Parser support disorder template tag
        for (final Object o : items) {
            if (o instanceof Element)
                parseItem(pgdef, parent, (Element) o, annHelper, bNativeContent, ParsingState.FIRST);
        }

        for (final Object o : items) {
            if (o instanceof Element) {
                breakLine = false;
                preNativeInfo = (NativeInfo) parseItem(pgdef, parent, (Element) o, annHelper, bNativeContent,
                        ParsingState.SECOND);
            } else if (o instanceof ProcessingInstruction) {
                breakLine = false;
                parse(pgdef, (ProcessingInstruction) o);
            } else if (o instanceof Comment) {
                breakLine = false;
                // keep the comment, like <!--[if lte IE 9]>
                if (parentlang.isNative() || isXHTML) {
                    String label = "<!--" + ((Item) o).getText() + "-->";
                    final ComponentInfo labelInfo = parentlang.newLabelInfo(parent, label);
                    labelInfo.addProperty("encode", "false", null);
                }
            } else if ((o instanceof Text) || (o instanceof CData)) {
                String label = ((Item) o).getText(), trimLabel = !isXHTML ? label.trim() : label; // do not trim with xhtml
                if (breakLine && (o instanceof Text) && isEmpty(label.trim())) {
                    // we need to merge the breakLine into the previous one to save memory
                    List<NodeInfo> children = parent.getChildren();
                    final String labelAttr = parentlang.getLabelAttribute();
                    for (Property prop : ((ComponentInfo) children.get(children.size() - 1)).getProperties()) {
                        if (prop.getName().equals(labelAttr)) {
                            prop.setRawValue(prop.getRawValue() + trimLabel);
                        }
                    }
                    continue;
                }
                if (isEmpty(label))
                    continue;

                if (bZkSwitch) {
                    if (isEmpty(trimLabel))
                        continue;
                    throw new UiException(message("Only <zk> can be used in <zk switch>", (Item) o));
                }

                //ZK-2760: if parent is xhtml and no other sibling and length of label is 0, don't create TextInfo to it
                if (isXHTML && items.size() <= 1 && isEmpty(trimLabel.trim())) {
                    continue; //don't create Text
                } else if (isEmpty(trimLabel) && (pi != null && !pi.isBlankPreserved() && !isNativeText(pi)))
                    continue; //Ignore blank text if no need to preserved
                else if (isEmpty(label.trim()) && !isAllBlankPreserved)
                    continue;

                if (!isXHTML && (o instanceof Text) && isEmpty(label.trim()))
                    breakLine = true;

                //consider as a label
                if (isNativeText(pi)) {

                    // trim the cdata when in the XHTML parser which will retain the syntax
                    String newLabel = label.trim();
                    if (newLabel.startsWith("<![CDATA[") && newLabel.endsWith("]]>")) {
                        label = newLabel.substring(9, newLabel.length() - 3);
                    }
                    // Bug ZK-2845: ShadowInfo does not allow TextInfo as child
                    if (!(parent instanceof ShadowInfo)) {
                        // It's possible to replace multiple whitespace characters with single space
                        new TextInfo(parent, isEmpty(trimLabel) ? " " : label);
                    }
                    //Don't trim if native (3.5.0)
                } else {
                    if (textAs != null) { //implies pi != null (parent is ComponentInfo)
                        if (!isEmpty(trimLabel))
                            if (textAsBuffer != null) //implies pi == parent
                                textAsBuffer.append(label); //concatenate all together
                            else if (!(parent instanceof TemplateInfo))
                                throw new UnsupportedOperationException(
                                        message("Not allowed in text-as", ((Item) o).getParent()));
                    } else { // shadow element shouldn't support empty LabelInfo
                        if (parent instanceof ShadowInfo) {
                            if (isEmpty(trimLabel))
                                continue; //ignore
                        }
                        if (isTrimLabel() && !parentlang.isRawLabel()) {
                            if (isEmpty(trimLabel))
                                continue; //ignore
                            label = trimLabel;
                        }
                        if (isXHTML && preNativeInfo != null && isEmpty(label.trim())) { //Merge break line between two NativeInfo when using zhtml
                            preNativeInfo.addEpilogChild(new TextInfo(null, label));
                        } else {
                            final ComponentInfo labelInfo = parentlang.newLabelInfo(parent, label);
                            if (isEmpty(trimLabel))
                                labelInfo.setReplaceableText(" "); //yes, it can be replaced by a text, and it's possible to replace multiple whitespace characters with single space
                        }
                    }
                }
            } else {
                breakLine = false;
            }
        }

        if (textAsBuffer != null) { //parent might be TempalteInfo
            String trimLabel = textAsBuffer.toString();

            // Bug ZK-1911
            if (pi == null || !pi.isBlankPreserved())
                trimLabel = trimLabel.trim();

            if (!isEmpty(trimLabel))
                pi.addProperty(textAs, trimLabel, null);
        }
    }

    /*package*/ static boolean isNativeText(ComponentInfo pi) { //also called by ComponentInfo
        if (pi instanceof NativeInfo)
            return true;

        if (pi != null) {
            try {
                final Class cls = pi.resolveImplementationClass(null, null);
                return cls != null && Native.class.isAssignableFrom(cls);
            } catch (Throwable ex) { //ignore
            }
        }
        return false;
    }

    /** Returns whether to trim the leading and trailing whitespaces
     * of labels.
     * <p>Default: false since 3.0.4.
     *
     * <p>If you want to trim like 3.0.4 and earlier did, you can specify
     * the system property called "org.zkoss.zk.ui.parser.trimLabel"
     * with a non-empty value.
     */
    private static boolean isTrimLabel() {
        if (_trimLabel == null) {
            final String s = Library.getProperty("org.zkoss.zk.ui.parser.trimLabel");
            _trimLabel = Boolean.valueOf(s != null && s.length() > 0);
        }
        return _trimLabel.booleanValue();
    }

    private static Boolean _trimLabel;

    private static final LanguageDefinition getLanguageDefinition(NodeInfo node) {
        for (; node != null; node = node.getParent()) {
            if (node instanceof ComponentInfo) {
                LanguageDefinition langdef = ((ComponentInfo) node).getLanguageDefinition();
                if (langdef != null)
                    return langdef;
            } else if (node instanceof PageDefinition) {
                return ((PageDefinition) node).getLanguageDefinition();
            } else if (node instanceof ShadowInfo) {
                return ((ShadowInfo) node).getLanguageDefinition();
            }
        }
        return null;
    }


    /** Parse an component definition specified in the given element.
     * @param bNativeContent whether to consider the child elements all native
     * It is true if a component definition with text-as is found
     * @return native item for optimization
     */
    private Object parseItem(PageDefinition pgdef, NodeInfo parent, Element el, AnnotationHelper annHelper,
            boolean bNativeContent, ParsingState parsingState) throws Exception {
        final String nm = el.getLocalName();
        final Namespace ns = el.getNamespace();
        final String pref = ns != null ? ns.getPrefix() : "";
        final String uri = ns != null ? ns.getURI() : "";
        LanguageDefinition langdef = pgdef.getLanguageDefinition();
        final String langName = langdef.getName();

        if (LanguageDefinition.ANNOTATION_NAMESPACE.equals(uri) || "annotation".equals(uri))
            throw new UiException(message("Namespace, " + uri + ", no longer supported element's annotation", el));

        //ZK-2632: Parser support disorder template tag
        if (parsingState != ParsingState.SECOND) {
            if ("attribute".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
                if (!(parent instanceof ComponentInfo))
                    throw new UiException(message("<attribute> cannot be the root element", el));
                parseAttribute(pgdef, (ComponentInfo) parent, el, annHelper);
            } else if ("template".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
                parseItems(pgdef, parseTemplate(parent, el, annHelper), el.getChildren(), annHelper, bNativeContent);
            }
        }

        if (parsingState != ParsingState.FIRST) {
            if (("attribute".equals(nm) || "template".equals(nm)) && isZkElement(langdef, nm, pref, uri))
                return null;
            if ("zscript".equals(nm) && isZkElement(langdef, nm, pref, uri)) {
                checkZScriptEnabled(el);
                parseZScript(parent, el, annHelper);
            } else if ("custom-attributes".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
                parseCustomAttributes(langdef, parent, el, annHelper);
            } else if ("variables".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
                parseVariables(langdef, parent, el, annHelper);
            } else if ("zk".equals(nm) && isZkElement(langdef, nm, pref, uri)) {
                parseItems(pgdef, parseZk(parent, el, annHelper), el.getChildren(), annHelper, bNativeContent);
            } else if (isShadowElement(langdef, pgdef, nm, pref, uri, bNativeContent)) {
                NodeInfo nodeInfo = parseShadowElement(pgdef, parent, el, annHelper);
                parseItems(pgdef, nodeInfo, el.getChildren(), annHelper, bNativeContent);
                if (nodeInfo instanceof ShadowInfo && ((ShadowInfo) nodeInfo).isAnnotationNamespacedRoot()) {
                    annHelper.setIgnoreAnnotNamespace(false);
                } else if (nodeInfo instanceof TemplateInfo && ((TemplateInfo) nodeInfo).isAnnotationNamespacedRoot()) {
                    annHelper.setIgnoreAnnotNamespace(false);
                }
            } else {
                //if (log.isDebugEnabled()) log.debug("component: "+nm+", ns:"+ns);
                if (isZkSwitch(parent))
                    throw new UiException(message("Only <zk> can be used in <zk switch>", el));

                boolean prefRequired = uri.startsWith(LanguageDefinition.NATIVE_NAMESPACE_PREFIX);
                boolean bNative = bNativeContent || prefRequired || LanguageDefinition.NATIVE_NAMESPACE.equals(uri)
                        || "native".equals(uri);

                if (!bNative && langdef.isNative() && !langdef.getNamespace().equals(uri))
                    bNative = prefRequired = ("".equals(pref) && "".equals(uri)) || !LanguageDefinition.exists(uri);
                //Spec: if pref/URI not specified => native
                //        if uri unknown => native

                final ComponentInfo compInfo;
                if (bNative) {
                    if (annHelper.clear())
                        log.warn(message("Annotations are ignored since native doesn't support them", el));

                    final NativeInfo ni;
                    compInfo = ni = new NativeInfo(parent, langdef.getNativeDefinition(),
                            prefRequired && pref.length() > 0 ? pref + ":" + nm : nm);

                    //add declared namespace if starting with native:
                    final Collection<Namespace> dns = el.getDeclaredNamespaces();
                    if (!dns.isEmpty())
                        addDeclaredNamespace(ni, dns, langdef);
                } else {
                    final boolean defaultNS = isDefaultNS(langdef, pref, uri);
                    final LanguageDefinition complangdef = defaultNS ? langdef : LanguageDefinition.lookup(uri);
                    ComponentDefinition compdef = defaultNS ? pgdef.getComponentDefinitionMap().get(nm) : null;
                    if (compdef != null) {
                        compInfo = new ComponentInfo(parent, compdef, nm);
                    } else if (complangdef.hasComponentDefinition(nm)) {
                        compdef = complangdef.getComponentDefinition(nm);
                        compInfo = new ComponentInfo(parent, compdef, nm);
                        langdef = complangdef;
                    } else {
                        compdef = complangdef.getDynamicTagDefinition();
                        if (compdef == null)
                            throw new DefinitionNotFoundException(
                                    message("Component definition not found: " + nm + " in " + complangdef, el));
                        compInfo = new ComponentInfo(parent, compdef, nm);
                        langdef = complangdef;
                    }

                    //process use first because addProperty needs it
                    String use = el.getAttributeValue("use");
                    if (use != null) {
                        use = use.trim();
                        if (use.length() != 0)
                            compInfo.setImplementation(use);
                        //Resolve later since might defined in zscript
                    }
                }

                String ifc = null, unless = null, forEach = null, forEachBegin = null, forEachEnd = null,
                        forEachStep = null;
                AnnotationHelper attrAnnHelper = null;
                //ZK 8: If the attribute of viewModel being used, auto apply "BindComposer"
                boolean isMVVM = false;
                boolean shouldIgnoreAnnotNamespace = annHelper.shouldIgnoreAnnotNamespace();
                for (final Attribute attr : el.getAttributeItems()) {
                    final Namespace attrns = attr.getNamespace();
                    final String attURI = attrns != null ? attrns.getURI() : "";
                    final String attnm = attr.getLocalName();
                    final String attval = attr.getValue();
                    final String attPref = attrns != null ? attrns.getPrefix() : "";

                    // ZK-2494: use getName instead of getLocalName when namespace is native or xml and attribute uri 
                    // is not one of known namespaces in zk. Exclude xmlns to avoid redefine.
                    if (isNativeNamespace(uri) || isXmlNamespace(uri) || "native".equals(langName)
                            || "xml".equals(langName)) {
                        if (!isZkAttr(langdef, attrns) && !isZKNamespace(attURI) && !"xmlns".equals(attPref)
                                && !("xmlns".equals(attnm) && "".equals(attPref))
                                && !"http://www.w3.org/2001/XMLSchema-instance".equals(attURI)) {

                            // Bug ZK-2995
                            boolean handled = false;
                            for (NamespaceParser nsParser : _nsParsers) {
                                if (nsParser.isMatched(attURI)) {
                                    if (nsParser.parse(attr, compInfo, pgdef)) {
                                        handled = true;
                                        break;
                                    }
                                }
                            }
                            if (!handled) {
                                compInfo.addProperty(attr.getName(), attval, null);
                            }
                            continue;
                        } else if (isClientNamespace(attURI) || isClientAttrNamespace(attURI)) {
                            compInfo.addProperty(attnm, attval, null);
                            continue;
                        } else if (isClientAttrPrefixNamespace(attURI)) { // ZK-4474
                            compInfo.addProperty(attPref + ":" + attnm, attval, null);
                            continue;
                        }
                    }

                    if ("apply".equals(attnm) && isZkAttr(langdef, attrns)) {
                        compInfo.setApply(attval);
                    } else if ("forward".equals(attnm) && isZkAttr(langdef, attrns)) {
                        compInfo.setForward(attval);
                    } else if ("if".equals(attnm) && isZkAttr(langdef, attrns)) {
                        ifc = attval;
                    } else if ("unless".equals(attnm) && isZkAttr(langdef, attrns)) {
                        unless = attval;
                    } else if ("forEach".equals(attnm) && isZkAttr(langdef, attrns)) {
                        forEach = attval;
                    } else if ("forEachStep".equals(attnm) && isZkAttr(langdef, attrns)) {
                        forEachStep = attval;
                    } else if ("forEachBegin".equals(attnm) && isZkAttr(langdef, attrns)) {
                        forEachBegin = attval;
                    } else if ("forEachEnd".equals(attnm) && isZkAttr(langdef, attrns)) {
                        forEachEnd = attval;
                    } else if ("fulfill".equals(attnm) && isZkAttr(langdef, attrns, bNativeContent)) {
                        compInfo.setFulfill(attval);
                    } else if (!shouldIgnoreAnnotNamespace && (LanguageDefinition.ANNOTATION_NAMESPACE.equals(attURI) || "annotation".equals(attURI))) {
                        //ZK 6: annotation namespace mandates annotation
                        if (attrAnnHelper == null)
                            attrAnnHelper = new AnnotationHelper();
                        applyAttrAnnot(attrAnnHelper, compInfo, attnm, attval.trim(), true, location(attr));
                    } else if (!"use".equals(attnm) || !isZkAttr(langdef, attrns, bNativeContent)) {
                        if (!"xmlns".equals(attPref) && !("xmlns".equals(attnm) && "".equals(attPref))
                                && !"http://www.w3.org/2001/XMLSchema-instance".equals(attURI)) {
                            String attvaltrim = attval.trim();
                            if ("viewModel".equals(attnm)) {
                                if (attval.indexOf("@id") == -1) {
                                    attvaltrim = "@id('vm') " + attvaltrim;
                                    attvaltrim = attvaltrim.replace("@(", "@init(");
                                }
                            } else {
                                attvaltrim = modifyAttrValueIfSimplified(attnm, attval.trim());
                            }
                            if (!bNativeContent && !bNative && (shouldIgnoreAnnotNamespace
                                    || (attURI.length() == 0 || LanguageDefinition.ZK_NAMESPACE.endsWith(attURI)))
                                    && AnnotationHelper.isAnnotation(attvaltrim)) {
                                if (attrAnnHelper == null)
                                    attrAnnHelper = new AnnotationHelper();
                                applyAttrAnnot(attrAnnHelper, compInfo, attnm, attvaltrim, true, location(attr));
                                //ZK 8: If the attribute of viewModel being used, auto apply "BindComposer"
                                Configuration config = WebApps.getCurrent().getConfiguration();
                                if (config.getBinderInitAttribute().equals(attnm))
                                    isMVVM = true;
                                //F80 - store subtree's binder annotation count
                                Set<String> binderAnnotations = config.getBinderAnnotations();
                                for (String annot : binderAnnotations) {
                                    if (attvaltrim.contains(annot)) {
                                        compInfo.enableBindingAnnotation();
                                        break;
                                    }
                                }
                            } else {
                                boolean handled = false;
                                for (NamespaceParser nsParser : _nsParsers) {
                                    if (nsParser.isMatched(attURI)) {
                                        if (nsParser.parse(attr, compInfo, pgdef)) {
                                            handled = true;
                                            break;
                                        }
                                    }
                                }
                                if (!handled) {
                                    addAttribute(compInfo, attrns, attnm, attval, null, attr.getLocator());
                                    if (attrAnnHelper != null)
                                        attrAnnHelper.applyAnnotations(compInfo, attnm, true);
                                }
                            }
                        }
                    }
                }

                //ZK 8: If the attribute of viewModel being used, auto apply "BindComposer"
                if (isMVVM) {
                    String apply = compInfo.getApply();
                    String bindComposerStr = Library.getProperty("org.zkoss.bind.defaultComposer.class", "org.zkoss.bind.BindComposer");
                    if (apply != null && apply.contains(bindComposerStr) && !apply.contains(",")) {
                        log.warn(message("If the attribute of viewModel is being used, then \"" + bindComposerStr
                                + "\" will be applied automatically. "
                                + "Notice that if you want to use another Composer, you will need to apply the \""
                                + bindComposerStr + "\" yourself.", el));
                    } else if (apply == null || apply.length() == 0) { //ZK-2763: only auto apply "BindComposer" when the attribute "apply" isn't used.
                        compInfo.setApply(bindComposerStr);
                    }
                }
                compInfo.setCondition(ConditionImpl.getInstance(ifc, unless));
                compInfo.setForEach(forEach, forEachBegin, forEachEnd, forEachStep);
                annHelper.applyAnnotations(compInfo, null, true);

                //only provide if there already has other annotation
                if (compInfo.getAnnotationMap() != null && el.getLocator() != null) {
                    //provide component location info as a annotation with it's location.
                    compInfo.addAnnotation(null, "ZKLOC", null, Locators.toLocation(el.getLocator()));
                }

                final Collection<Item> items = el.getChildren();
                String textAs = null;
                if (!bNativeContent && !items.isEmpty() //if empty, no need to check
                        && (textAs = compInfo.getTextAs()) != null) {
                    //if textAs is specified, we detect if any child element
                    //if so, we parse them as property
                    //if not, we handle it normally and text, if any, will be
                    //trimmed and assigned as a property (in parseItems)
                    if (compInfo.isChildAllowedInTextAs() //don't consider it as text if childable and element found
                            || !textAsAllowed(langdef, items, bNativeContent))
                        textAs = null;
                }

                if (textAs != null)
                    parseAsProperty(pgdef, compInfo, textAs, items, annHelper, null);
                else
                    parseItems(pgdef, compInfo, items, annHelper, bNativeContent);

                //optimize native components
                if (compInfo instanceof NativeInfo && !compInfo.getChildren().isEmpty()) {
                    optimizeNativeInfos((NativeInfo) compInfo);
                    return compInfo;
                }
            } //end-of-else//
        }
        return null;
    }

    private static String modifyAttrValueIfSimplified(String attrName, String attrValue) {
        boolean isCommand = attrValue.matches("@(global-)?command\\(.*\\)");
        if (attrValue.matches("@\\(.*\\)") || isCommand) {
            if (Events.isValid(attrName) || isCommand) {
                int attrValueIndex = attrValue.indexOf("(") + 1;
                String commandProperty = attrValue.substring(attrValueIndex, attrValue.length() - 1);
                boolean isNamedParam = false;
                int len = commandProperty.length();
                final StringBuilder modifiedCommandPropertySb = new StringBuilder(len);
                final StringBuilder sb = new StringBuilder(len);
                String nm = null;
                char quot = (char) 0;
                int paramIndex = -1;
                int inRoundBrackets = 0;
                for (int j = 0;; ++j) {
                    if (j >= len) {
                        modifiedCommandPropertySb.append(modifyAttrValueIfSimplified0(nm, sb.toString().trim(), paramIndex, isNamedParam));
                        break; //done
                    }
                    char cc = commandProperty.charAt(j);
                    if (quot == (char) 0) {
                        if (cc == ',') {
                            sb.append(cc);
                            modifiedCommandPropertySb.append(modifyAttrValueIfSimplified0(nm, sb.toString().trim(), paramIndex, isNamedParam));
                            if (paramIndex != 0 && !isNamedParam && nm != null) { // named param and un-named parameters together
                                throwCommandSimplifiedErrorUsage();
                            }
                            isNamedParam = nm != null;
                            nm = null; //cleanup
                            sb.setLength(0); //cleanup
                            paramIndex++;
                            continue; //next name=value
                        } else if (cc == '=' && inRoundBrackets == 0) {
                            nm = sb.toString().trim(); //name found
                            sb.setLength(0); //cleanup
                            continue; //parse value
                        } else if (cc == '\'' || cc == '"') {
                            quot = cc;
                        } else if (cc == '(') {
                            inRoundBrackets++;
                        } else if (cc == ')') {
                            inRoundBrackets--;
                        }
                    } else if (cc == quot) {
                        quot = (char) 0;
                    }
                    sb.append(cc);
                    if (cc == '\\' && j < len - 1)
                        sb.append(commandProperty.charAt(++j));
                    //Note: we don't decode \x. Rather, we preserve it such
                    //that the data binder can use them
                }
                if (!isCommand) {
                    attrValue = "@command(" + modifiedCommandPropertySb + ")";
                } else {
                    attrValue = attrValue.substring(0, attrValueIndex) + modifiedCommandPropertySb + ")";
                }
            } else
                attrValue = "@bind" + attrValue.substring(1);
        }
        return attrValue;
    }

    public static final String SIMPLIFIED_COMMAND_PARAM_PREFIX = "zk_Param_";

    private static String modifyAttrValueIfSimplified0(String nm, String val, int paramIndex, boolean isNamedParam) {
        if (nm == null) {
            if (isNamedParam)
                throwCommandSimplifiedErrorUsage();
            if (paramIndex != -1) //skip command method name
                val = SIMPLIFIED_COMMAND_PARAM_PREFIX + paramIndex + "=" + val;
        } else {
            val = nm + "=" + val;
        }
        return val;
    }

    private static void throwCommandSimplifiedErrorUsage() {
        throw new UiException("Not allowed to use named parameters and un-named parameters together.");
    }

    private boolean textAsAllowed(LanguageDefinition langdef, Collection<Item> items, boolean bNativeContent) {
        boolean textAsAllowed = true;
        String xmlFound = null; //whether a XML fragment
        String zkElem = null; //a ZK element
        boolean empty = true; //whether there is anything other than whitespace
        for (Iterator<Item> it = items.iterator();;) {
            if (zkElem != null && xmlFound != null)
                throw new UnsupportedOperationException(message(
                        "Unable to handle XML fragment, <" + xmlFound + ">, with <" + zkElem
                                + ">. Please use CDATA instead",
                        (it.hasNext() ? it.next() : items.iterator().next().getParent())));
            //might be possible to handle but not worth
            if (!it.hasNext())
                break;

            final Item o = it.next();
            if (empty)
                empty = (o instanceof Text || o instanceof CData) && o.getText().trim().length() == 0;

            if (o instanceof Element) {
                final Element e = (Element) o;
                final String n = e.getLocalName();
                if (isZkElement(langdef, e, bNativeContent) && ("attribute".equals(n) || "custom-attributes".equals(n)
                        || "variables".equals(n) || "template".equals(n) || "zscript".equals(n))) { //we have to skip zscript because of B50-3259479
                    zkElem = n;
                    textAsAllowed = false;
                    //unable to handle them because EL/zscript might affect
                    //the result
                } else {
                    xmlFound = n;
                    //including "zk" (risk if allowed to mix zk with attribute...)
                }
            }
        }
        return textAsAllowed && !empty;
        //if empty (i.e., only whitespace), so don't handle textAs (i.e., ignore it)
    }

    /** Parses the items as if they are native and they will become a property
     * rather than child components.
     */
    private void parseAsProperty(PageDefinition pgdef, ComponentInfo compInfo, String name, Collection items,
            AnnotationHelper annHelper, ConditionImpl cond) throws Exception {
        final NativeInfo nativeInfo = new NativeInfo(compInfo.getEvaluatorRef(),
                pgdef.getLanguageDefinition().getNativeDefinition(), "");
        //Note: nativeInfo can not be a child. Rather, it will be a property
        parseItems(pgdef, nativeInfo, items, annHelper, true);
        compInfo.addProperty(name, nativeInfo, cond);
    }

    /** @param val the value (it was trimmed before called). */
    private static void applyAttrAnnot(AnnotationHelper attrAnnHelper, ComponentInfo compInfo, String nm, String val,
            boolean selfAllowed, org.zkoss.util.resource.Location loc) {
        attrAnnHelper.addByCompoundValue(val.trim(), loc);
        attrAnnHelper.applyAnnotations(compInfo, selfAllowed && "self".equals(nm) ? null : nm, true);
    }

    /** @param val the value (it was trimmed before called). */
    private static void applyAttrAnnot(AnnotationHelper attrAnnHelper, ShadowInfo compInfo, String nm, String val,
            boolean selfAllowed, org.zkoss.util.resource.Location loc) {
        attrAnnHelper.addByCompoundValue(val.trim(), loc);
        attrAnnHelper.applyAnnotations(compInfo, selfAllowed && "self".equals(nm) ? null : nm, true);
    }

    private static void warnWrongZkAttr(Attribute attr) {
        log.warn(message("Attribute " + attr.getName() + " ignored in <zk>", attr));
    }

    private static boolean isZkSwitch(NodeInfo nodeInfo) {
        return nodeInfo instanceof ZkInfo && ((ZkInfo) nodeInfo).withSwitch();
    }

    private void parseZScript(NodeInfo parent, Element el, AnnotationHelper annHelper) {
        if (el.getAttributeItem("forEach") != null)
            throw new UiException(message("forEach not applicable to <zscript>", el));
        if (annHelper.clear())
            log.warn(message("Annotations are ignored since <zscript> doesn't support them", el));

        final String ifc = el.getAttributeValue("if"), unless = el.getAttributeValue("unless"),
                zsrc = el.getAttributeValue("src");
        final boolean deferred = "true".equals(el.getAttributeValue("deferred"));

        String zslang = el.getAttributeValue("language");
        if (zslang == null) {
            zslang = parent.getPageDefinition().getZScriptLanguage();
            //we have to resolve it in parser since a page might be
            //created by use of createComponents
        } else {
            noEmpty("language", zslang, el);
            noEL("language", zslang, el);
        }

        final ConditionImpl cond = ConditionImpl.getInstance(ifc, unless);
        if (!isEmpty(zsrc)) { //ignore empty (not error)
            ZScriptInfo zs = null;
            if (!zsrc.contains("${")) {
                final URL url = getLocator().getResource(zsrc);
                if (url != null)
                    zs = new ZScriptInfo(parent, zslang, url, cond);
                //Bug 2929887: defer the error message since it might not be required
            }
            if (zs == null)
                zs = new ZScriptInfo(parent, zslang, zsrc, getLocator(), cond);

            if (deferred)
                zs.setDeferred(true);
        }

        String script = el.getText(false);
        if (!isEmpty(script.trim())) {
            final org.zkoss.xml.Locator l = el.getLocator();
            int lno = l != null ? l.getLineNumber() : 0;
            if (lno > 1) {
                final StringBuilder sb = new StringBuilder(lno);
                while (--lno > 0)
                    sb.append('\n');
                script = sb.append(script).toString();
            }
            final ZScriptInfo zs = new ZScriptInfo(parent, zslang, script, cond);
            if (deferred)
                zs.setDeferred(true);
        }
    }

    private void parseAttribute(PageDefinition pgdef, ComponentInfo parent, Element el, AnnotationHelper annHelper)
            throws Exception {
        if (el.getAttributeItem("forEach") != null)
            throw new UiException(message("forEach not applicable to attribute", el));

        //Test if any element is used
        String elFound = null;
        for (final Item o : el.getChildren()) {
            if (o instanceof Element) {
                elFound = ((Element) o).getName();
                break;
            }
        }

        final Attribute attr = el.getAttributeItem(null, "name", 0); //by local name
        if (attr == null)
            throw new UiException(message("The name attribute required", el));

        final String attnm = attr.getValue();
        noEmpty("name", attnm, el);
        final ConditionImpl cond = ConditionImpl.getInstance(el.getAttributeValue("if"),
                el.getAttributeValue("unless"));
        if (elFound != null) {
            if (Events.isValid(attnm))
                throw new UiException(message("<" + elFound + "> not allowed in an event listener", el));

            parseAsProperty(pgdef, parent, attnm, el.getChildren(), annHelper, cond);
        } else {
            final String trim = el.getAttributeValue("trim");
            noEL("trim", trim, el);
            final String attval = el.getText(trim != null && "true".equals(trim));
            addAttribute(parent, attr.getNamespace(), attnm, attval, cond, el.getLocator());
        }

        annHelper.applyAnnotations(parent, attnm, true);
    }

    private static void parseCustomAttributes(LanguageDefinition langdef, NodeInfo parent, Element el,
            AnnotationHelper annHelper) throws Exception {
        //if (!el.getElements().isEmpty())
        //    throw new UiException(message("Child elements are not allowed for <custom-attributes>", el));

        if (parent instanceof PageDefinition)
            throw new UiException(message("<custom-attributes> must be used under a component", el));
        if (annHelper.clear())
            log.warn(message("Annotations are ignored since <custom-attributes> doesn't support them", el)); //old style annotation not supported

        String ifc = null, unless = null, scope = null, composite = null;
        final Map<String, String> attrs = new LinkedHashMap<String, String>();
        AnnotationHelper attrAnnHelper = null;
        for (final Attribute attr : el.getAttributeItems()) {
            final Namespace attrns = attr.getNamespace();
            final String attnm = attr.getLocalName();
            final String attval = attr.getValue();
            final String attvaltrim;
            if ("if".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                ifc = attval;
            } else if ("unless".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                unless = attval;
            } else if ("scope".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                scope = attval;
            } else if ("composite".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                composite = attval;
            } else if ("forEach".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                throw new UiException(message("forEach not applicable to <custom-attributes>", el));
            } else if (parent instanceof ComponentInfo && AnnotationHelper.isAnnotation(attvaltrim = attval.trim())) {
                if (attrAnnHelper == null)
                    attrAnnHelper = new AnnotationHelper();
                applyAttrAnnot(attrAnnHelper, (ComponentInfo) parent, attnm, attvaltrim, false, location(attr));
            } else {
                attrs.put(attnm, attval);
            }
        }

        if (!attrs.isEmpty())
            new AttributesInfo(parent, attrs, scope, composite, ConditionImpl.getInstance(ifc, unless));
    }

    private static void parseVariables(LanguageDefinition langdef, NodeInfo parent, Element el,
            AnnotationHelper annHelper) throws Exception {
        //if (!el.getElements().isEmpty())
        //    throw new UiException(message("Child elements are not allowed for <variables> element", el));

        if (annHelper.clear())
            log.warn(message("Annotations are ignored since <variables> doesn't support them", el)); //old style annotation not supported here

        String ifc = null, unless = null, composite = null;
        boolean local = false;
        final Map<String, String> vars = new LinkedHashMap<String, String>();
        for (final Attribute attr : el.getAttributeItems()) {
            final Namespace attrns = attr.getNamespace();
            final String attnm = attr.getLocalName();
            final String attval = attr.getValue();
            if ("if".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                ifc = attval;
            } else if ("unless".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                unless = attval;
            } else if ("local".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                local = "true".equals(attval);
            } else if ("composite".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                composite = attval;
            } else if ("forEach".equals(attnm) && isZkElementAttr(langdef, attrns)) {
                throw new UiException(message("forEach not applicable to <variables>", el));
            } else {
                vars.put(attnm, attval);
            }
        }
        if (!vars.isEmpty())
            new VariablesInfo(parent, vars, local, composite, ConditionImpl.getInstance(ifc, unless));
    }

    private static NodeInfo parseShadowElement(PageDefinition pgdef, NodeInfo parent, Element el,
            AnnotationHelper annHelper) throws Exception {
        String ifc = null, unless = null, name = el.getLocalName();
        AnnotationHelper attrAnnHelper = null;
        final LanguageDefinition lookup = LanguageDefinition.lookup("xul/html");
        ComponentDefinition shadowDefinition = lookup.hasShadowDefinition(name) ? lookup.getShadowDefinition(name)
                : pgdef.getComponentDefinitionMap().get(name);
        final ShadowInfo compInfo = new ShadowInfo(parent, shadowDefinition, name,
                ConditionImpl.getInstance(ifc, unless));
        boolean annotated = false;
        boolean shouldIgnoreAnnotNamespace = annHelper.shouldIgnoreAnnotNamespace();
        for (final Attribute attr: el.getAttributeItems()) {
            final Namespace attrns = attr.getNamespace();
            final String attURI = attrns != null ? attrns.getURI() : "";
            final String attnm = attr.getLocalName();
            final String attval = attr.getValue();
            if ("if".equals(attnm)) {
                ifc = attval;
            } else if ("unless".equals(attnm)) {
                unless = attval;
            } else if (!shouldIgnoreAnnotNamespace && (LanguageDefinition.ANNOTATION_NAMESPACE.equals(attURI) || "annotation".equals(attURI))) {
                annotated = true;
                //ZK 6: annotation namespace mandates annotation
                if (attrAnnHelper == null)
                    attrAnnHelper = new AnnotationHelper();
                applyAttrAnnot(attrAnnHelper, compInfo, attnm, attval.trim(), false, location(attr));
            } else {
                final String attPref = attrns != null ? attrns.getPrefix() : null;
                String attvaltrim;
                if (!"xmlns".equals(attPref) && !("xmlns".equals(attnm) && "".equals(attPref))
                        && !"http://www.w3.org/2001/XMLSchema-instance".equals(attURI)) {
                    attvaltrim = modifyAttrValueIfSimplified(attnm, attval.trim());
                    if ((attURI.length() == 0 || LanguageDefinition.ZK_NAMESPACE.endsWith(attURI))
                            && AnnotationHelper.isAnnotation(attvaltrim)) { // annotation
                        if (attrAnnHelper == null)
                            attrAnnHelper = new AnnotationHelper();
                        applyAttrAnnot(attrAnnHelper, compInfo, attnm, attvaltrim, true, location(attr));
                        //F80 - store subtree's binder annotation count
                        Set<String> binderAnnotations = WebApps.getCurrent().getConfiguration().getBinderAnnotations();
                        for (String annot : binderAnnotations) {
                            if (attvaltrim.contains(annot)) {
                                compInfo.enableBindingAnnotation();
                                annotated = true;
                                break;
                            }
                        }
                    } else {
                        compInfo.addProperty(attnm, attval, null);
                        if (attrAnnHelper != null)
                            attrAnnHelper.applyAnnotations(compInfo, attnm, true);
                    }
                }
            }
        }

        compInfo.setCondition(ConditionImpl.getInstance(ifc, unless));
        annHelper.applyAnnotations(compInfo, null, true);

        Node root = el.getFirstChild();
        while (root != null && !(root instanceof Element)) {
            if (root instanceof Text) {
                if (!isEmpty(((Text) root).getText().trim()))
                    break;
            }
            root = root.getNextSibling();
        }
        if (root != null) {
            String rn = root.getLocalName();
            if (!"template".equals(rn)) { // automatically create a template tag for it.
                Node item = root;
                while (item != null) {
                    if ("template".equals(item.getLocalName()))
                        throw new UiException(message(rn + " does not support between templates", (Item) root));
                    item = item.getNextSibling();
                }
                TemplateInfo templateInfo = new TemplateInfo(compInfo, "", null, null, null);
                if (!shouldIgnoreAnnotNamespace && annotated) {
                    annHelper.setIgnoreAnnotNamespace(true);
                    templateInfo.setAnnotationNamespacedRoot(true);
                }
                return templateInfo;
            }
        }
        if (!shouldIgnoreAnnotNamespace && annotated) {
            annHelper.setIgnoreAnnotNamespace(true);
            compInfo.setAnnotationNamespacedRoot(true);
        }
        return compInfo;
    }

    private static TemplateInfo parseTemplate(NodeInfo parent, Element el, AnnotationHelper annHelper)
            throws Exception {
        if (annHelper.clear())
            log.warn(message("Annotations are ignored since <template> doesn't support them", el));
        if (el.getAttributeItem("forEach") != null)
            log.warn(message("forEach is ignored since <template> doesn't support it", el));

        String ifc = null, unless = null, name = null, src = null;
        final Map<String, String> params = new LinkedHashMap<String, String>(); //reserve the order
        for (final Attribute attr : el.getAttributeItems()) {
            final Namespace attrns = attr.getNamespace();
            final String attURI = attrns != null ? attrns.getURI() : "";
            final String attnm = attr.getLocalName();
            final String attval = attr.getValue();
            if ("name".equals(attnm)) {
                name = attval;
            } else if ("src".equals(attnm)) {
                src = attval;
            } else if ("if".equals(attnm)) {
                ifc = attval;
            } else if ("unless".equals(attnm)) {
                unless = attval;
            } else {
                final String attPref = attrns != null ? attrns.getPrefix() : null;
                if (!"xmlns".equals(attnm) && !"xml".equals(attnm) && !attURI.contains("w3.org")
                        && (attPref == null || (!"xmlns".equals(attPref) && !"xml".equals(attPref))))
                    params.put(attnm, attval);
            }
        }
        if (name == null) {
            //throw new UiException(message("The name attribute required", el));
            name = "";
            // support "" an empty string for ZK 8.0.0 shadow element

        }
        return new TemplateInfo(parent, name, src, params, ConditionImpl.getInstance(ifc, unless));
    }

    private static ZkInfo parseZk(NodeInfo parent, Element el, AnnotationHelper annHelper) throws Exception {
        if (annHelper.clear())
            log.warn(message("Annotations are ignored since <zk> doesn't support them", el));

        final ZkInfo zi = new ZkInfo(parent, null);
        String ifc = null, unless = null, forEach = null, forEachBegin = null, forEachEnd = null, forEachStep = null;
        for (final Attribute attr : el.getAttributeItems()) {
            final Namespace attrns = attr.getNamespace();
            final String attURI = attrns != null ? attrns.getURI() : "";
            final String attnm = attr.getLocalName();
            final String attval = attr.getValue();
            if ("if".equals(attnm) || "when".equals(attnm)) {
                ifc = attval;
            } else if ("unless".equals(attnm)) {
                unless = attval;
            } else if ("forEach".equals(attnm)) {
                forEach = attval;
            } else if ("forEachStep".equals(attnm)) {
                forEachStep = attval;
            } else if ("forEachBegin".equals(attnm)) {
                forEachBegin = attval;
            } else if ("forEachEnd".equals(attnm)) {
                forEachEnd = attval;
            } else if ("switch".equals(attnm) || "choose".equals(attnm)) {
                if (isZkSwitch(parent))
                    throw new UiException(message("<zk " + attnm + "> cannot be used in <zk switch/choose>", el));
                zi.setSwitch(attval);
            } else if ("case".equals(attnm)) {
                if (!isZkSwitch(parent))
                    throw new UiException(message("<zk case> can be used only in <zk switch>", attr));
                zi.setCase(attval);
            } else {
                final String attPref = attrns != null ? attrns.getPrefix() : null;
                if (!"xmlns".equals(attnm) && !"xml".equals(attnm) && !attURI.contains("w3.org")
                        && (attPref == null || (!"xmlns".equals(attPref) && !"xml".equals(attPref))))
                    warnWrongZkAttr(attr);
            }
        }
        zi.setCondition(ConditionImpl.getInstance(ifc, unless));
        zi.setForEach(forEach, forEachBegin, forEachEnd, forEachStep);
        return zi;
    }

    /** Whether a string is null or empty. */
    private static boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }

    /** Whether the name space belongs to the default language. */
    private static final boolean isDefaultNS(LanguageDefinition langdef, String pref, String uri) {
        return (!langdef.isNative() && "".equals(pref) && "".equals(uri)) || langdef.getNamespace().equals(uri);
    }

    /** Returns whether it is a Shadow element.
     * @param pref namespace's prefix
     * @param uri namespace's URI
     * @param bNativeContent whether to ignore if URI not specified explicitly
     */
    private static final boolean isShadowElement(LanguageDefinition langdef, PageDefinition pgdef, String nm,
            String pref, String uri, boolean bNativeContent) {
        // feature in 8.0.0, no need to check namespace, if any.
        if ("true".equalsIgnoreCase(Library.getProperty("org.zkoss.zk.namespace.tolerant", "false")))
            return langdef.hasShadowDefinition(nm) || (!"xul/html".equals(langdef.getName())
                    && LanguageDefinition.lookup("xul/html").hasShadowDefinition(nm));
        ComponentDefinition componentDefinition = pgdef.getComponentDefinitionMap().get(nm);
        // F80 - support another namespace
        if (componentDefinition instanceof ShadowDefinitionImpl || LanguageDefinition.SHADOW_NAMESPACE.equals(uri)
                || LanguageDefinition.SHADOW_NAME.equals(uri))
            return true;
        if (isDefaultNS(langdef, pref, uri))
            return !bNativeContent && langdef.hasShadowDefinition(nm);
        return false;
    }

    /** Returns whether it is a ZK element.
     * @param pref namespace's prefix
     * @param uri namespace's URI
     * @param bNativeContent whether to ignore if URI not specified explicitly
     */
    private static final boolean isZkElement(LanguageDefinition langdef, String nm, String pref, String uri,
            boolean bNativeContent) {
        // feature in 8.0.0, no need to check namespace, if any.
        if ("true".equalsIgnoreCase(Library.getProperty("org.zkoss.zk.namespace.tolerant", "false")))
            return true;
        if (isDefaultNS(langdef, pref, uri))
            return !bNativeContent && !langdef.hasComponentDefinition(nm);
        return LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri);
    }

    private static final boolean isZkElement(LanguageDefinition langdef, Element el, boolean bNativeContent) {
        final Namespace ns = el.getNamespace();
        return isZkElement(langdef, el.getLocalName(), ns != null ? ns.getPrefix() : "", ns != null ? ns.getURI() : "",
                bNativeContent);
    }

    private static final boolean isZkElement(LanguageDefinition langdef, String nm, String pref, String uri) {
        return isZkElement(langdef, nm, pref, uri, false);
    }

    /** Returns whether it is a ZK attribute (in a non-ZK element).
     * @param bNativeContent whether to ignore if URI not specified explicitly
     */
    private static final boolean isZkAttr(LanguageDefinition langdef, Namespace attrns, boolean bNativeContent) {
        //if native we will make sure URI is ZK or lang's namespace
        if ((bNativeContent || langdef.isNative()) && attrns != null && "".equals(attrns.getPrefix()))
            return false; //if navtive, "" means not ZK

        return isZkElementAttr(langdef, attrns);
    }

    private static final boolean isZkAttr(LanguageDefinition langdef, Namespace attrns) {
        return isZkAttr(langdef, attrns, false);
    }

    /** Similar to {@link #isZkAttr}, except it doesn't care isNative
     * (i.e., we consider it as a ZK attribute unless a namespace
     * other than ZK is specified.
     */
    private static final boolean isZkElementAttr(LanguageDefinition langdef, Namespace attrns) {
        if (attrns == null //not possible; just in case
                || "".equals(attrns.getPrefix())) //w3c attr: uri is "" if prefix ""
            return true;

        final String uri = attrns.getURI();
        return LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri) || langdef.getNamespace().equals(uri);
    }

    /** Parse an attribute and adds it to the definition.
     */
    private void addAttribute(ComponentInfo compInfo, Namespace attrns, String name, String value, ConditionImpl cond,
            org.zkoss.xml.Locator xl) throws Exception {
        if (Events.isValid(name)) {
            boolean bZkAttr = attrns == null;
            if (!bZkAttr) {
                final String uri = attrns.getURI();
                if (LanguageDefinition.CLIENT_NAMESPACE.equals(uri) || "client".equals(uri)) {
                    compInfo.addWidgetListener(name, value, cond);
                    return;
                }
                if (LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri) || "client/attribute".equals(uri)) {
                    compInfo.addWidgetAttribute(name, value, cond);
                    return;
                }

                final String pref = attrns.getPrefix();
                LanguageDefinition langdef = compInfo.getLanguageDefinition();
                ComponentDefinition compdef = compInfo.getComponentDefinition();

                if (langdef == null)
                    bZkAttr = true;
                // refix ZK-2495: skip native components
                else if (isDefaultNS(langdef, pref, uri) && !compdef.isNative())
                    bZkAttr = !langdef.isDynamicReservedAttributes("[event]");
                else
                    bZkAttr = LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri);
            }
            if (bZkAttr) {
                checkZScriptEnabled(xl);
                final int lno = xl != null ? xl.getLineNumber() : 0;
                final ZScript zscript = ZScript.parseContent(value, lno);
                if (zscript.getLanguage() == null)
                    zscript.setLanguage(compInfo.getPageDefinition().getZScriptLanguage());
                //resolve it here instead of runtime since createComponents
                compInfo.addEventHandler(name, zscript, cond);
                return; //done
            }
        } else {
            final String uri = attrns.getURI();
            if (LanguageDefinition.CLIENT_NAMESPACE.equals(uri) || "client".equals(uri)) {
                if (name.length() == 0)
                    throw new UiException(message("Client attribute name required", xl));
                if ("use".equals(name)) {
                    if (cond != null)
                        throw new UnsupportedOperationException(message("if and unless not allowed for w:use", xl));
                    compInfo.setWidgetClass(value);
                } else {
                    compInfo.addWidgetOverride(name, value, cond);
                }
                return;
            }
            if (LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri) || "client/attribute".equals(uri)) {
                compInfo.addWidgetAttribute(name, value, cond);
                return;
            }
        }

        // ZK-4474
        if (attrns != null) {
            if (isClientAttrPrefixNamespace(attrns.getURI())) {
                compInfo.addWidgetAttribute(attrns.getPrefix() + ":" + name, value, cond);
                return;
            }
        }
        
        compInfo.addProperty(name, value, cond);
    }

    /** Adds the declared namespaces to the native info, if necessary.
     */
    private static void addDeclaredNamespace(NativeInfo nativeInfo, Collection<Namespace> namespaces,
            LanguageDefinition langdef) {
        for (Namespace ns : namespaces) {
            final String uri = ns.getURI();
            boolean bNatPrefix = uri.startsWith(LanguageDefinition.NATIVE_NAMESPACE_PREFIX);
            // ZK-2494: Should also consider adding declaredNamespace inside a native component
            if (bNatPrefix || (!isZKNamespace(uri) && !langdef.getNamespace().equals(uri)))
                nativeInfo.addDeclaredNamespace(new Namespace(ns.getPrefix(),
                        bNatPrefix ? uri.substring(LanguageDefinition.NATIVE_NAMESPACE_PREFIX.length()) : uri));
        }
    }

    /** Minimizes the native infos such that UiEngine creates
     * the minimal number of components.
     */
    private static void optimizeNativeInfos(NativeInfo compInfo) {
        //Optimize 1: merge to prolog, if the first children are
        //native and have no child
        for (Iterator it = compInfo.getChildren().iterator(); it.hasNext();) {
            final NodeInfo o = (NodeInfo) it.next();
            if (o instanceof NativeInfo) {
                final NativeInfo childInfo = (NativeInfo) o;
                if (!childInfo.getChildren().isEmpty())
                    break;
                childInfo.setParentDirectly(null);

                // merge annotation if any
                AnnotationMap annotationMap = childInfo.getAnnotationMap();
                if (annotationMap != null) {
                    for (String propName : annotationMap.getAnnotatedProperties()) {
                        for (Annotation anno : annotationMap.getAnnotations(propName)) {
                            compInfo.addAnnotation(propName, anno.getName(), anno.getAttributes(), anno.getLocation());
                        }
                    }
                }
            } else if (o instanceof ComponentInfo || o instanceof ZkInfo || o instanceof ShadowInfo) {
                break;
            }

            compInfo.addPrologChild(o);
            it.remove(); //detach it from the children list
        }

        //Optimize 2: merge to epilog if the last children, are
        //native and have no child
        int sz = compInfo.getChildren().size();
        if (sz >= 0) {
            final ListIterator it = compInfo.getChildren().listIterator(sz);
            while (it.hasPrevious()) {
                final Object o = it.previous();
                if (o instanceof NativeInfo) {
                    final NativeInfo childInfo = (NativeInfo) o;
                    if (!childInfo.getChildren().isEmpty()) {
                        it.next();
                        break;
                    }
                    childInfo.setParentDirectly(null);
                } else if (o instanceof ComponentInfo || o instanceof ZkInfo || o instanceof ShadowInfo) {
                    it.next();
                    break;
                }
            }
            while (it.hasNext()) {
                final NodeInfo o = (NodeInfo) it.next();
                compInfo.addEpilogChild(o);
                it.remove();
            }
        }

        //Optimize 3: merge to split child
        //If there is only one native child, we make it a split child and
        //make all its children (grand-children) up one level
        if (compInfo.getChildren().size() == 1 && compInfo.getSplitChild() == null /*just in case*/) {
            Iterator it = compInfo.getChildren().iterator();
            final Object o = it.next();
            if (o instanceof NativeInfo) {
                final NativeInfo childInfo = (NativeInfo) o;
                //FUTURE: enhance UiEngineImpl to handle split's forEach
                if (!childInfo.withForEach() && !childInfo.withCondition()) { // B65-ZK-1626
                    childInfo.setParentDirectly(null);
                    compInfo.setSplitChild(childInfo);
                    it.remove();

                    for (it = new ArrayList<NodeInfo>(childInfo.getChildren()).iterator(); it.hasNext();)
                        compInfo.appendChild((NodeInfo) it.next());
                }
            }
        }
    }

    /** Returns the language definition of the specified name or namespace.
     *  Return null if Definition not found
     */
    private static LanguageDefinition langdefLookup(String name) {
        if (name == null || name.length() == 0) {
            return null;
        }

        try {
            return LanguageDefinition.lookup(name);
        } catch (DefinitionNotFoundException e) {
            return null;
        }
    }

    /** Returns whether the given uri is a known namespace in ZK or not
     */
    private static boolean isZKNamespace(String uri) {
        return LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri)
                || LanguageDefinition.NATIVE_NAMESPACE.equals(uri) || "native".equals(uri) || "annotation".equals(uri)
                || LanguageDefinition.ANNOTATION_NAMESPACE.equals(uri) || "client".equals(uri)
                || LanguageDefinition.CLIENT_NAMESPACE.equals(uri) || "client/attribute".equals(uri)
                || LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri) || "xhtml".equals(uri)
                || "http://www.w3.org/1999/xhtml/".equals(uri) || "zul".equals(uri)
                || "http://www.zkoss.org/2005/zul/".equals(uri) || "xml".equals(uri)
                || "http://www.zkoss.org/2007/xml".equals(uri) || isClientAttrPrefixNamespace(uri) || langdefLookup(uri) != null;
    }

    /** Returns whether the given uri is in native namespace or not
     */
    private static boolean isNativeNamespace(String uri) {
        return "native".equals(uri) || LanguageDefinition.NATIVE_NAMESPACE.equals(uri);
    }

    /** Returns whether the given uri is in xml namespace or not
     */
    private static boolean isXmlNamespace(String uri) {
        return "xml".equals(uri) || "http://www.zkoss.org/2007/xml".equals(uri);
    }

    /** Returns whether the given uri is in client namespace or not
     */
    private static boolean isClientNamespace(String uri) {
        return "client".equals(uri) || LanguageDefinition.CLIENT_NAMESPACE.equals(uri);
    }

    /** Returns whether the given uri is in client attribute namespace or not
     */
    private static boolean isClientAttrNamespace(String uri) {
        return "client/attribute".equals(uri) || LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri);
    }

    /** Returns whether the given uri is in client raw attribute namespace or not
     */
    private static boolean isClientAttrPrefixNamespace(String uri) {
        return LanguageDefinition.CLIENT_ATTRIBUTE_PREFIX_NAME.equals(uri) || LanguageDefinition.CLIENT_ATTRIBUTE_PREFIX_NAMESPACE.equals(uri);
    }

    //ZK-2632: Parser support disorder template tag
    private enum ParsingState {
        ROOT, FIRST, SECOND
    }
}