/* PageDefinition.java

	Purpose:
		
	Description:
		
	History:
		Tue May 31 11:27:07     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.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.zkoss.html.HTMLs;
import org.zkoss.lang.ClassResolver;
import org.zkoss.lang.ImportedClassResolver;
import org.zkoss.util.resource.Locator;
import org.zkoss.xel.ExpressionFactory;
import org.zkoss.xel.Expressions;
import org.zkoss.xel.Function;
import org.zkoss.xel.FunctionMapper;
import org.zkoss.xel.VariableResolver;
import org.zkoss.xel.taglib.FunctionDefinition;
import org.zkoss.xel.taglib.Taglib;
import org.zkoss.xel.taglib.Taglibs;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.sys.PageConfig;
import org.zkoss.zk.ui.sys.PageCtrl;
import org.zkoss.zk.ui.util.Initiator;
import org.zkoss.zk.xel.Evaluator;
import org.zkoss.zk.xel.EvaluatorRef;
import org.zkoss.zk.xel.ExValue;
import org.zkoss.zk.xel.impl.SimpleEvaluator;

/**
 * A page definition.
 * It represents a ZUL page.
 *
 * <p>Note: it is not thread-safe.
 *
 * <p>Note: it is not serializable.
 *
 * @author tomyeh
 * @see ComponentDefinition
 */
public class PageDefinition implements NodeInfo {
	/** A list of a children ({@link NodeInfo}). */
	private final List<NodeInfo> _children = new LinkedList<NodeInfo>();
	private final LanguageDefinition _langdef;
	private final Locator _locator;
	private String _id, _title, _style, _viewport;
	/** The request path. */
	private String _path = "";
	/** The zscript language. */
	private String _zslang = "Java";
	private List<Taglib> _taglibs;
	/** A map of imported classes for expression, Map<String nm, Class cls>. */
	private Map<String, Class<?>> _expimps;
	/** A list of XEL methods, List<FunctionDefinition>. */
	private List<FunctionDefinition> _xelfuncs;
	/** The evaluator. */
	private Evaluator _eval;
	/** The evaluator reference. */
	private EvaluatorRef _evalr;
	/** The function mapper. */
	private FunctionMapper _mapper;
	/* List(InitiatorInfo). */
	private List<InitiatorInfo> _initdefs;
	/** List(VariableResolverInfo). */
	private List<VariableResolverInfo> _resolvdefs;
	/** List(FunctionMapperInfo mapper). */
	private List<FunctionMapperInfo> _mapperdefs;
	/** List(HeaderInfo). They are generated before ZK default headers, such as meta. */
	private List<HeaderInfo> _hdBfrDefs;
	/** List(HeaderInfo). They are generated after ZK default headers. */
	private List<HeaderInfo> _hdAftDefs;
	/** List(ResponseHeaderInfo). */
	private List<ResponseHeaderInfo> _hdResDefs;
	/** List(ForwardInfo). */
	private List<ForwardInfo> _forwdefs;
	/** Map(String name, ExValue value). */
	private Map<String, ExValue> _rootAttrs;
	private ExValue _contentType, _docType, _firstLine, _wgtcls;
	/** The class of the expression factory (ExpressionFactory).*/
	private Class<? extends ExpressionFactory> _expfcls;
	/** The class resolver. */
	private final ImportedClassResolver _clsresolver = new ImportedClassResolver();
	private final ComponentDefinitionMap _compdefs;
	private Boolean _cacheable;
	private Boolean _autoTimeout;
	private boolean _complete;

	//ZK-2623: page scope template
	private Map<String, TemplateInfo> _templatesInfo;

	/** Constructor.
	 * @param langdef the default language which is used if no namespace
	 * is specified. Note: a page might have components from different
	 * languages.
	 */
	public PageDefinition(LanguageDefinition langdef, Locator locator) {
		if (langdef == null)
			throw new IllegalArgumentException("null langdef");
		if (locator == null)
			throw new IllegalArgumentException("null locator");

		_langdef = langdef;
		_locator = locator;
		_compdefs = new ComponentDefinitionMap(_langdef.getComponentDefinitionMap().isCaseInsensitive());
	}

	/** Constructor.
	 * @param langdef the default language which is used if no namespace
	 * is specified. Note: a page might have components from different
	 * languages.
	 * @param id the identifier. See {@link #setId}.
	 * @param title the title. See {@link #setTitle}.
	 * @param style the CSS style. See {@link #setStyle}.
	 */
	public PageDefinition(LanguageDefinition langdef, String id, String title, String style, Locator locator) {
		this(langdef, locator);
		setId(id);
		setTitle(title);
		setStyle(style);
	}

	/** Returns the language definition that this page is default to be.
	 */
	public LanguageDefinition getLanguageDefinition() {
		return _langdef;
	}

	/** Returns the locator associated with this page definition.
	 */
	public Locator getLocator() {
		return _locator;
	}

	/** Returns the default scripting language which is assumed when
	 * a zscript element doesn't specify any language.
	 *
	 * <p>Default: Java.
	 */
	public String getZScriptLanguage() {
		return _zslang;
	}

	/** Sets the default scripting language which is assumed when
	 * a zscript element doesn't specify any language.
	 *
	 * @param zslang the default scripting language.
	 */
	public void setZScriptLanguage(String zslang) {
		if (zslang == null || zslang.length() == 0)
			throw new IllegalArgumentException("null or empty");
		_zslang = zslang;
	}

	/** Returns the identifier that will be assigned to pages created from
	 * this definition, or null if the identifier shall be generated automatically.
	 * <p>Note: the returned value might contain EL expressions.
	 */
	public String getId() {
		return _id;
	}

	/** Sets the identifier that will be assigned to pages created from this
	 * definition.
	 * @param id the identifier. It might contain EL expressions.
	 * If null or empty (null is assumed), page's ID is generated automatically.
	 * If not empty, ID (after evaluated) must be unique in the same request.
	 */
	public void setId(String id) {
		_id = id != null && id.length() > 0 ? id : null;
	}

	/** Returns the title that will be assigned to pages created from
	 * this definition, or null if no title is assigned at the beginning.
	 * <p>Note: the returned value might contain EL expressions.
	 */
	public String getTitle() {
		return _title;
	}

	/** Sets the title that will be assigned to pages created from
	 * this definition, or null if no title is assigned at the beginning.
	 * @param title the title. If empty, null is assumed.
	 */
	public void setTitle(String title) {
		_title = title != null && title.length() > 0 ? title : null;
	}

	/** Returns the CSS style that will be assigned to pages created from
	 * this definition, or null if no style is assigned at the beginning.
	 * <p>Note: the returned value might contain EL expressions.
	 */
	public String getStyle() {
		return _style;
	}

	/** Sets the CSS style that will be assigned to pages created from
	 * this definition, or null if no style is assigned at the beginning.
	 * @param style the CSS style. If empty, null is assumed.
	 */
	public void setStyle(String style) {
		_style = style != null && style.length() > 0 ? style : null;
	}

	/** Returns the viewport that will be assigned to pages created from
	 * this definition, or "auto" if no viewport is assigned at the beginning.
	 * <p>Note: the returned value might contain EL expressions.
	 * @since 6.5.0
	 */
	public String getViewport() {
		return _viewport;
	}

	/** Sets the viewport that will be assigned to pages created from
	 * this definition, or "auto" if no viewport is assigned at the beginning.
	 * @param viewport the viewport setting. If empty, "auto" is assumed.
	 * @since 6.5.0
	 */
	public void setViewport(String viewport) {
		_viewport = viewport != null && viewport.length() > 0 ? viewport : "auto";
	}

	/** Returns the widget class of the given page, or null if the default is used.
	 * @since 5.0.5
	 */
	public String getWidgetClass(Page page) {
		return _wgtcls != null ? (String) _wgtcls.getValue(_evalr, page) : null;
	}

	/** Sets the widget class of the page.
	 * @param wgtcls the widget class. It may contain EL expressions.
	 * If null or empty, the default widget class is assumed.
	 * @since 5.0.5
	 */
	public void setWidgetClass(String wgtcls) {
		_wgtcls = wgtcls != null && wgtcls.length() > 0 ? new ExValue(wgtcls, String.class) : null;
	}

	/** Returns the request path of this page definition, or ""
	 * if not available.
	 * <p>It is the same as the servlet path
	 * (javax.servlet.http.HttpServletRequest's getServletPath), if ZK is running
	 * at a servlet container.
	 */
	public String getRequestPath() {
		return _path;
	}

	/** Sets the request path of this page definition.
	 */
	public void setRequestPath(String path) {
		_path = path != null ? path : "";
	}

	/** Imports the specified directives from the specified page definition.
	 *
	 * @param pgdef the page definition to import from.
	 * @param directives an array of the directive names to import.
	 * If null, {"init", "component"} is assumed, i.e., only the init
	 * directives and component definitions are imported.<br/>
	 * Importable directives include "component", "init", "meta",
	 * "taglib", "variable-resolver", and "xel-method".
	 * If "*", all of them are imported.
	 * Note: "meta" implies "link".
	 * @since 3.0.2
	 */
	public void imports(PageDefinition pgdef, String[] directives) {
		if (directives == null || contains(directives, "import"))
			_clsresolver.addAll(pgdef._clsresolver);

		if (pgdef._initdefs != null && (directives == null || contains(directives, "init")))
			for (InitiatorInfo ii : pgdef._initdefs)
				addInitiatorInfo(ii);

		if (directives == null || contains(directives, "component")) {
			for (Iterator it = pgdef._compdefs.getNames().iterator(); it.hasNext();)
				addComponentDefinition(pgdef._compdefs.get((String) it.next()));
		}

		if (pgdef._taglibs != null && directives != null && contains(directives, "taglib"))
			for (Taglib tl : pgdef._taglibs)
				addTaglib(tl);

		if (pgdef._resolvdefs != null && directives != null && contains(directives, "variable-resolver")) {
			for (VariableResolverInfo vri : pgdef._resolvdefs)
				addVariableResolverInfo(vri);
		}

		if (pgdef._mapperdefs != null && directives != null && contains(directives, "function-mapper")) {
			for (FunctionMapperInfo fmi : pgdef._mapperdefs)
				addFunctionMapperInfo(fmi);
		}

		if (pgdef._xelfuncs != null && directives != null && contains(directives, "xel-method"))
			for (FunctionDefinition xfi : pgdef._xelfuncs)
				addXelMethod(xfi.prefix, xfi.name, xfi.function);

		if (pgdef._hdBfrDefs != null && directives != null && contains(directives, "meta")) {
			for (HeaderInfo hi : pgdef._hdBfrDefs)
				addHeaderInfo(hi, true);
		}
		if (pgdef._hdAftDefs != null && directives != null && contains(directives, "meta")) {
			for (HeaderInfo hi : pgdef._hdAftDefs)
				addHeaderInfo(hi, false);
		}

		if (pgdef._hdResDefs != null && directives != null && contains(directives, "header")) {
			for (ResponseHeaderInfo rhi : pgdef._hdResDefs)
				addResponseHeaderInfo(rhi);
		}
	}

	/** Imports the init directives and component definitions from
	 * the specified page definition.
	 *
	 * <p>It is the same as imports(pgdef, null).
	 *
	 * @since #imports(PageDefinition, String[])
	 */
	public void imports(PageDefinition pgdef) {
		imports(pgdef, null);
	}

	private static boolean contains(String[] dirs, String dir) {
		for (int j = dirs.length; --j >= 0;)
			if ("*".equals(dirs[j]) || dir.equalsIgnoreCase(dirs[j]))
				return true;
		return false;
	}

	/** Adds an imported class
	 * Like Java, it is used to import a class or a package of classes, so
	 * that it simplifies the use of the apply attribute, the init directive
	 * and others.
	 * 
	 * @param clsptn the class's fully-qualified name, e.g., <code>com.foo.FooComposer</code>,
	 * a wildcard representing all classes of the give package, e.g., <code>com.foo.*</code>.
	 * @since 6.0.0
	 */
	public void addImportedClass(String clsptn) throws ClassNotFoundException {
		_clsresolver.addImportedClass(clsptn);
	}

	/** Returns a readonly list of the imported class names.
	 * @since 6.0.0
	 */
	public List<String> getImportedClasses() {
		return _clsresolver.getImportedClasses();
	}

	/** Returns the class resolver represented by {@link #getImportedClasses}.
	 * @since 6.0.0
	 */
	public ClassResolver getImportedClassResolver() {
		return _clsresolver;
	}

	/** Adds a definition of {@link Initiator}. */
	public void addInitiatorInfo(InitiatorInfo init) {
		if (init == null)
			throw new IllegalArgumentException("null");

		if (_initdefs == null)
			_initdefs = new LinkedList<InitiatorInfo>();
		_initdefs.add(init);
	}

	/** Returns a list of all {@link Initiator} and invokes
	 * its {@link Initiator#doInit} before returning.
	 * It never returns null.
	 */
	public List<Initiator> doInit(Page page) {
		if (_initdefs == null)
			return Collections.emptyList();

		final List<Initiator> inits = new LinkedList<Initiator>();
		try {
			for (InitiatorInfo ii : _initdefs) {
				final Initiator init = ii.newInitiator(getEvaluator(), page);
				if (init != null)
					inits.add(init);
			}
		} catch (Throwable ex) {
			throw UiException.Aide.wrap(ex);
		}
		return inits;
	}

	/** Adds a definition of {@link VariableResolver}.
	 */
	public void addVariableResolverInfo(VariableResolverInfo resolver) {
		if (resolver == null)
			throw new IllegalArgumentException("null");

		if (_resolvdefs == null)
			_resolvdefs = new LinkedList<VariableResolverInfo>();
		_resolvdefs.add(resolver);
	}

	/** Adds a definition of {@link FunctionMapper}.
	 * @since 3.5.0
	 */
	public void addFunctionMapperInfo(FunctionMapperInfo mapper) {
		if (mapper == null)
			throw new IllegalArgumentException("null");

		if (_mapperdefs == null)
			_mapperdefs = new LinkedList<FunctionMapperInfo>();
		_mapperdefs.add(mapper);
	}

	/** Adds a XEL method.
	 *
	 * @param prefix the prefix of the method name
	 * @param name the method name. The final name is "prefix:name"
	 * @param func the function.
	 * @since 3.0.0
	 */
	public void addXelMethod(String prefix, String name, Function func) {
		checkXelModifiable();
		if (name == null || prefix == null || func == null)
			throw new IllegalArgumentException();
		if (_xelfuncs == null)
			_xelfuncs = new LinkedList<FunctionDefinition>();
		_xelfuncs.add(new FunctionDefinition(prefix, name, func));
	}

	/** Adds a response header.
	 * @since 5.0.2
	 */
	public void addResponseHeaderInfo(ResponseHeaderInfo header) {
		if (header == null)
			throw new IllegalArgumentException();
		if (_hdResDefs == null)
			_hdResDefs = new LinkedList<ResponseHeaderInfo>();
		_hdResDefs.add(header);
	}

	/** Returns a map of response headers (never null).
	 * The value of each entry is a two-element object array. The
	 * first element of the array is the value which is an instance of {@link java.util.Date}
	 * or {@link String} (and never null).
	 * The second element indicates whether to add (rather than set)
	 * header. It is an instance of Boolean (and never null).
	 */
	public Collection<Object[]> getResponseHeaders(Page page) {
		final List<Object[]> headers = new LinkedList<Object[]>();
		if (_hdResDefs != null)
			for (ResponseHeaderInfo rhi : _hdResDefs) {
				headers.add(new Object[] { rhi.getName(), rhi.getValue(this, page),
						Boolean.valueOf(rhi.shallAppend(this, page)) });
			}
		return headers;
	}

	/** Adds a header definition ({@link HeaderInfo}).
	 * @param before whether to place the header <b>before</b> ZK's
	 * CSS/JS headers. If false, it is placed after ZK's CSS/JS headers.
	 * @since 3.6.1
	 */
	public void addHeaderInfo(HeaderInfo header, boolean before) {
		if (header == null)
			throw new IllegalArgumentException();

		if (before) {
			if (_hdBfrDefs == null)
				_hdBfrDefs = new LinkedList<HeaderInfo>();
			_hdBfrDefs.add(header);
		} else {
			if (_hdAftDefs == null)
				_hdAftDefs = new LinkedList<HeaderInfo>();
			_hdAftDefs.add(header);
		}
	}

	/** Adds a header definition ({@link HeaderInfo}).
	 * It places the meta headers before ZK's CSS/JS headers,
	 * and others after ZK's CSS/JS headers.
	 */
	public void addHeaderInfo(HeaderInfo header) {
		addHeaderInfo(header, "meta".equals(header.getName()));
	}

	/** Returns the content that shall be generated inside the head element
	 * and before ZK's default tags (never null).
	 * For example, it might consist of &lt;meta&gt; and &lt;link&gt;.
	 *
	 * <p>Since it is generated before ZK's default tags (such as CSS and JS),
	 * it cannot override ZK's default behaviors.
	 *
	 * @see #getAfterHeadTags
	 * @since 5.0.5
	 */
	public String getBeforeHeadTags(Page page) {
		return getHeadTags(page, _hdBfrDefs);
	}

	/** Returns the content that shall be generated inside the head element
	 * and after ZK's default tags (never null).
	 * For example, it might consist of &lt;meta&gt; and &lt;link&gt;.
	 *
	 * <p>Since it is generated after ZK's default tags (such as CSS and JS),
	 * it could override ZK's default behaviors.
	 *
	 * @see #getBeforeHeadTags
	 * @since 5.0.5
	 */
	public String getAfterHeadTags(Page page) {
		return getHeadTags(page, _hdAftDefs);
	}

	private String getHeadTags(Page page, List<HeaderInfo> defs) {
		if (defs == null)
			return "";

		final StringBuffer sb = new StringBuffer(256);
		for (HeaderInfo hi : defs) {
			final String s = hi.toHTML(this, page);
			if (s != null && s.length() > 0)
				sb.append(s).append('\n');
		}
		return sb.toString();
	}

	/** Adds a forward definition ({@link ForwardInfo}).
	 */
	public void addForwardInfo(ForwardInfo forward) {
		if (forward == null)
			throw new IllegalArgumentException("null");

		if (_forwdefs == null)
			_forwdefs = new LinkedList<ForwardInfo>();
		_forwdefs.add(forward);
	}

	/** Returns the URI to forward to, or null if not to forward.
	 * It evaluates the forward definition (added by {@link #addForwardInfo})
	 * one-by-one, if any, to see whether to forward.
	 * Returns null if no forward definition, or no forward definition's
	 * condition is satisfied.
	 */
	public String getForwardURI(Page page) {
		if (_forwdefs == null)
			return null;

		for (ForwardInfo fi : _forwdefs) {
			final String uri = fi.resolveURI(this, page);
			if (uri != null)
				return uri;
		}
		return null;
	}

	/** Returns the content type (after evaluation),
	 * or null to use the device default.
	 *
	 * @param page the page used to evaluate EL expressions, if any
	 * @since 3.0.0
	 */
	public String getContentType(Page page) {
		return _contentType != null ? (String) _contentType.getValue(_evalr, page) : null;
	}

	/** Sets the content type.
	 *
	 * <p>Default: null (use the device default).
	 *
	 * @param contentType the content type. It may contain EL expressions.
	 * @since 3.0.0
	 */
	public void setContentType(String contentType) {
		_contentType = contentType != null && contentType.length() > 0 ? new ExValue(contentType, String.class) : null;
	}

	/** Returns the doc type (&lt;!DOCTYPE&gt;) (after evaluation),
	 * or null to use the device default.
	 *
	 * @param page the page used to evaluate EL expressions, if any
	 * @since 3.0.0
	 */
	public String getDocType(Page page) {
		return _docType != null ? (String) _docType.getValue(_evalr, page) : null;
	}

	/** Sets the doc type (&lt;!DOCTYPE&gt;).
	 *
	 * <p>Default: null (use the device default).
	 *
	 * @param docType the doc type. It may contain EL expressions.
	 * If null, it means device's default will be used.
	 * If empty, it means no doc type will be generated.
	 * @since 3.0.0
	 */
	public void setDocType(String docType) {
		_docType = docType != null ? new ExValue(docType, String.class) : null;
	}

	/** Returns the first line to be generated to the output
	 * (after evaluation), or null if nothing to generate.
	 *
	 * <p>For XML devices, it is usually the xml processing instruction:<br/>
	 * <code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
	 *
	 * @param page the page used to evaluate EL expressions, if any
	 * @since 3.0.0
	 */
	public String getFirstLine(Page page) {
		return _firstLine != null ? (String) _firstLine.getValue(_evalr, page) : null;
	}

	/** Sets the first line to be generated to the output.
	 *
	 * <p>Default: null (i.e., nothing generated)
	 * @since 3.0.0
	 */
	public void setFirstLine(String firstLine) {
		_firstLine = firstLine != null && firstLine.length() > 0 ? new ExValue(firstLine, String.class) : null;
	}

	/** Returns if the client can cache the rendered result, or null
	 * to use the device default.
	 *
	 * @since 3.0.0
	 */
	public Boolean getCacheable() {
		return _cacheable;
	}

	/** Sets if the client can cache the rendered result.
	 *
	 * <p>Default: null (use the device default).
	 * @since 3.0.0
	 */
	public void setCacheable(Boolean cacheable) {
		_cacheable = cacheable;
	}

	/** Returns whether to automatically redirect to the timeout URI.
	 *
	 * @see #setAutomaticTimeout
	 * @since 3.6.3
	 */
	public Boolean getAutomaticTimeout() {
		return _autoTimeout;
	}

	/** Sets whether to automatically redirect to the timeout URI.
	 *
	 * <p>Default: null (use the device default).
	 * Refer to {@link org.zkoss.zk.ui.util.Configuration#setAutomaticTimeout}
	 * for details.
	 * @since 3.6.3
	 */
	public void setAutomaticTimeout(Boolean autoTimeout) {
		_autoTimeout = autoTimeout;
	}

	/** Returns if the page is a complete page.
	 * By complete we mean the page has everything that the client expects.
	 * For example, for HTML output, the page will generate
	 * the HTML, HEAD and BODY tags.
	 *
	 * <p>By default (false), we assume a page is complete if and only if
	 * it is <em>not</em> included by other page.
	 *
	 * <p>If you have a page that has a complete HTML page and
	 * it is included by other page, you have to specify the complete flag
	 * to be true.
	 * @since 3.0.4
	 */
	public boolean isComplete() {
		return _complete;
	}

	/** Sets if the page is a complete page.
	 * <p>Default: false. It means a page is complete if and only if it
	 * is <em>not</em> included by other page.
	 *
	 * @param complete whether the page is complete.
	 * If true, this page is assumed to be complete no matter it is included
	 * or not. If false, this page is assumed to be complete if it is
	 * not included by other page.
	 * @see #isComplete
	 * @since 3.0.4
	 */
	public void setComplete(boolean complete) {
		_complete = complete;
	}

	/** Adds a root attribute.
	 * The previous attribute of the same name will be replaced.
	 *
	 * @param value the value. If null, the attribute is removed.
	 * It can be an EL expression.
	 * @since 3.0.0
	 */
	public void setRootAttribute(String name, String value) {
		if (name == null || name.length() == 0)
			throw new IllegalArgumentException();

		if (_rootAttrs == null) {
			if (value == null)
				return; //nothing to
			_rootAttrs = new LinkedHashMap<String, ExValue>();
		}

		if (value == null)
			_rootAttrs.remove(name);
		else
			_rootAttrs.put(name, new ExValue(value, String.class));
	}

	/** Converts the header definitions (added by {@link #setRootAttribute})
	 * to the attributes of the root element.
	 * For HTML, the root element is the HTML element.
	 * @since 3.0.0
	 */
	public String getRootAttributes(Page page) {
		if (_rootAttrs == null || _rootAttrs.isEmpty())
			return "";

		final Evaluator eval = getEvaluator();
		final StringBuffer sb = new StringBuffer(256);
		for (Map.Entry<String, ExValue> me : _rootAttrs.entrySet()) {
			final String val = (String) me.getValue().getValue(eval, page);
			if (val != null)
				HTMLs.appendAttribute(sb, me.getKey(), val);
		}
		return sb.toString();
	}

	/** Returns the map of component definition (never null).
	 */
	public ComponentDefinitionMap getComponentDefinitionMap() {
		return _compdefs;
	}

	/** Adds a component definition belonging to this page definition only.
	 *
	 * <p>It is the same as calling {@link ComponentDefinitionMap#add} 
	 * against {@link #getComponentDefinitionMap}
	 */
	public void addComponentDefinition(ComponentDefinition compdef) {
		final LanguageDefinition langdef = compdef.getLanguageDefinition();
		if (langdef != null) {
			final LanguageDefinition ld2 = getLanguageDefinition();
			if (langdef != ld2 && !langdef.getDeviceType().equals(ld2.getDeviceType()))
				throw new UiException("Component definition, " + compdef
						+ ", does not belong to the same device type of the page definition, " + ld2.getDeviceType());
		}
		_compdefs.add(compdef);
	}

	/** Returns the component definition of the specified name, or null
	 * if not found.
	 *
	 * <p>Note: unlike {@link LanguageDefinition#getComponentDefinition},
	 * this method doesn't throw ComponentNotFoundException if not found.
	 * It just returns null.
	 *
	 * @param recurse whether to look up the component from {@link #getLanguageDefinition}
	 */
	public ComponentDefinition getComponentDefinition(String name, boolean recurse) {
		ComponentDefinition compdef = _compdefs.get(name);
		if (!recurse || compdef != null)
			return compdef;

		compdef = _langdef.getComponentDefinitionIfAny(name);
		if (compdef == null)
			compdef = _langdef.getShadowDefinitionIfAny(name);
		return compdef;
	}

	/** Returns the component definition of the specified class, or null
	 * if not found.
	 *
	 * <p>Note: unlike {@link LanguageDefinition#getComponentDefinition},
	 * this method doesn't throw ComponentNotFoundException if not found.
	 * It just returns null.
	 *
	 * @param recurse whether to look up the component from {@link #getLanguageDefinition}
	 */
	public ComponentDefinition getComponentDefinition(Class cls, boolean recurse) {
		ComponentDefinition compdef = _compdefs.get(cls);
		if (!recurse || compdef != null)
			return compdef;

		compdef = _langdef.getComponentDefinitionIfAny(cls);
		if (compdef == null)
			compdef = _langdef.getShadowDefinitionIfAny(cls);
		return compdef;
	}

	/** Adds a tag lib. */
	public void addTaglib(Taglib taglib) {
		checkXelModifiable();
		if (taglib == null)
			throw new IllegalArgumentException("null");

		if (_taglibs == null)
			_taglibs = new LinkedList<Taglib>();
		_taglibs.add(taglib);
	}

	/** Adds an imported class to the expression factory.
	 * @since 3.0.0
	 */
	public void addExpressionImport(String nm, Class<?> cls) {
		checkXelModifiable();
		if (nm == null || cls == null)
			throw new IllegalArgumentException();

		if (_expimps == null)
			_expimps = new HashMap<String, Class<?>>(4);
		_expimps.put(nm, cls);
	}

	private void checkXelModifiable() {
		if (_eval != null || _mapper != null)
			throw new IllegalStateException("getEvaluator() has been called, and no further change is allowed");
	}

	/** Sets the implementation of the expression factory that shall
	 * be used by this page.
	 *
	 * <p>Default: null (use the default).
	 *
	 * @param expfcls the implementation class, or null to use the default.
	 * Note: expfcls must implement {@link ExpressionFactory}.
	 * If null is specified, the class defined in
	 * {@link org.zkoss.zk.ui.util.Configuration#getExpressionFactoryClass}
	 * @exception IllegalArgumentException if expfcls does not implement ExpressionFactory
	 * @since 3.0.0
	 */
	@SuppressWarnings("unchecked")
	public void setExpressionFactoryClass(Class<?> expfcls) {
		checkXelModifiable();
		if (expfcls != null && !ExpressionFactory.class.isAssignableFrom(expfcls))
			throw new IllegalArgumentException(ExpressionFactory.class + " must be implemented: " + expfcls);
		_expfcls = (Class) expfcls;
	}

	/** Returns the implementation of the expression factory that
	 * is used by this page, or null if
	 * {@link org.zkoss.zk.ui.util.Configuration#getExpressionFactoryClass}
	 * is used.
	 *
	 * @see #setExpressionFactoryClass
	 * @since 3.0.0
	 */
	public Class<? extends ExpressionFactory> getExpressionFactoryClass() {
		return _expfcls;
	}

	// (NodeInfo)
	public Evaluator getEvaluator() {
		if (_eval == null)
			_eval = newEvaluator();
		return _eval;
	}

	private Evaluator newEvaluator() {
		return new SimpleEvaluator(getTaglibMapper(), _expfcls);
	}

	// (NodeInfo)
	public EvaluatorRef getEvaluatorRef() {
		if (_evalr == null)
			_evalr = newEvaluatorRef();
		return _evalr;
	}

	private EvaluatorRef newEvaluatorRef() {
		return new PageEvalRef(this);
	}

	/** Returns the mapper representing the functions defined in
	 * taglib and xel-method.
	 *
	 * <p>Note: it doesn't include the function mapper defined added by
	 * {@link #addFunctionMapperInfo}.
	 * @since 3.5.0
	 */
	public FunctionMapper getTaglibMapper() {
		if (_mapper == null) {
			_mapper = Taglibs.getFunctionMapper(_taglibs, _expimps, _xelfuncs, _locator);
			if (_mapper == null)
				_mapper = Expressions.EMPTY_MAPPER;
		}
		return _mapper != Expressions.EMPTY_MAPPER ? _mapper : null;
	}

	/** Adds a template info
	 * The previous src of the same name will be replaced.
	 *
	 * @param info the src. If null, the attribute is removed.
	 * @since 8.0.0
	 */
	public void addTemplateInfo(TemplateInfo info) {
		if (info == null)
			throw new IllegalArgumentException();

		if (_templatesInfo == null)
			_templatesInfo = new LinkedHashMap<String, TemplateInfo>();

		_templatesInfo.put(info.getName(), info);
	}

	/** Gets templates info map
	 * @since 8.0.0
	 */
	public Map<String, TemplateInfo> getTemplatesInfo() {
		return _templatesInfo;
	}

	/** Initializes the context for the given page before rendering
	 * this page definition.
	 * <p>It is called before {@link Initiator#doInit} and {@link #init}.
	 *
	 * @param page the page to initialize the context. It cannot be null.
	 */
	public void preInit(Page page) {
		page.addClassResolver(_clsresolver);

		page.addFunctionMapper(getTaglibMapper());

		if (_mapperdefs != null)
			for (Iterator it = _mapperdefs.iterator(); it.hasNext();) {
				final FunctionMapperInfo def = (FunctionMapperInfo) it.next();
				try {
					FunctionMapper mapper = def.newFunctionMapper(this, page);
					if (mapper != null)
						page.addFunctionMapper(mapper);
				} catch (Throwable ex) {
					throw UiException.Aide.wrap(ex);
				}
			}

		if (_resolvdefs != null)
			for (Iterator it = _resolvdefs.iterator(); it.hasNext();) {
				final VariableResolverInfo def = (VariableResolverInfo) it.next();
				try {
					VariableResolver resolver = def.newVariableResolver(this, page);
					if (resolver != null)
						page.addVariableResolver(resolver);
				} catch (Throwable ex) {
					throw UiException.Aide.wrap(ex);
				}
			}
	}

	/** Initializes a page after execution is activated.
	 * It setup the identifier and title, and adds it to desktop.
	 * <p>It is called after {@link #preInit} and {@link Initiator#doInit}.
	 */
	public void init(final Page page, final boolean evalHeaders) {
		final PageCtrl pageCtrl = (PageCtrl) page;
		pageCtrl.init(new PageConfig() {
			public String getId() {
				return _id;
			}

			public String getUuid() {
				return null;
			}

			public String getTitle() {
				return _title;
			}

			public String getStyle() {
				return _style;
			}

			public String getViewport() {
				return _viewport;
			}

			public String getBeforeHeadTags() {
				return evalHeaders ? PageDefinition.this.getBeforeHeadTags(page) : "";
			}

			public String getAfterHeadTags() {
				return evalHeaders ? PageDefinition.this.getAfterHeadTags(page) : "";
			}

			public Collection<Object[]> getResponseHeaders() {
				if (evalHeaders)
					return PageDefinition.this.getResponseHeaders(page);
				return Collections.emptyList();
			}
		});

		String s = getRootAttributes(page);
		if (s != null)
			pageCtrl.setRootAttributes(s);

		s = getContentType(page);
		if (s != null)
			pageCtrl.setContentType(s);

		s = getDocType(page);
		if (s != null)
			pageCtrl.setDocType(s);

		s = getFirstLine(page);
		if (s != null)
			pageCtrl.setFirstLine(s);

		s = getWidgetClass(page);
		if (s != null)
			pageCtrl.setWidgetClass(s);

		if (_cacheable != null)
			pageCtrl.setCacheable(_cacheable);
		if (_autoTimeout != null)
			pageCtrl.setAutomaticTimeout(_autoTimeout);
		if (_expfcls != null)
			page.setExpressionFactoryClass(_expfcls);
		page.setComplete(_complete);
	}

	//NodeInfo//
	/** Returns the parent (always null).
	 */
	public NodeInfo getParent() {
		return null;
	}

	/** Returns the page definition (always this).
	 */
	public PageDefinition getPageDefinition() {
		return this;
	}

	public void appendChild(NodeInfo child) {
		NodeInfo oldp = child.getParent();
		if (oldp != null)
			oldp.removeChild(child);

		_children.add(child);
		((LeafInfo) child).setParentDirectly(this); //except root, all are LeafInfo
		BranchInfo.fixEvaluatorRefDown(child, getEvaluatorRef());
		//Use getEvaluatorRef() to force _evalr being assigned
	}

	public boolean removeChild(NodeInfo child) {
		if (child != null && _children.remove(child)) {
			((LeafInfo) child).setParentDirectly(null); //except root, all are LeafInfo
			BranchInfo.fixEvaluatorRefDown(child, null);
			return true;
		}
		return false;
	}

	public List<NodeInfo> getChildren() {
		return _children;
	}

	//Object//
	public String toString() {
		return "[PageDefinition: " + (_id != null ? _id : _title != null ? _title : "" + System.identityHashCode(this))
				+ ", path=" + _path + ']';
	}
}
