/* HtmlPageRenders.java

	Purpose:

	Description:

	History:
		Tue Oct 14 19:06:37     2008, Created by tomyeh

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

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

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.owasp.encoder.Encode;

import org.zkoss.html.HTMLs;
import org.zkoss.html.JavaScript;
import org.zkoss.html.StyleSheet;
import org.zkoss.io.Files;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.mesg.Messages;
import org.zkoss.util.Pair;
import org.zkoss.web.fn.ServletFns;
import org.zkoss.web.fn.ThemeFns;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.device.Device;
import org.zkoss.zk.device.Devices;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.Sessions;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.ext.Includer;
import org.zkoss.zk.ui.http.WpdExtendlet;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;
import org.zkoss.zk.ui.util.Configuration;
import org.zkoss.zk.ui.util.DataHandlerInfo;
import org.zkoss.zk.ui.util.SimpleThemeURIModifier;
import org.zkoss.zk.ui.util.ThemeProvider;
import org.zkoss.zk.ui.util.ThemeURIHandler;
import org.zkoss.zk.ui.util.ThemeURIModifier;

/**
 * Utilities for implementing HTML-based {@link PageRenderer}.
 *
 * @author tomyeh
 * @since 5.0.0
 */
public class HtmlPageRenders {
	//	private static final Logger log = LoggerFactory.getLogger(HtmlPageRenders.class);

	/** Denotes whether style sheets are generated for this request. */
	private static final String ATTR_LANG_CSS_GENED = "javax.zkoss.zk.lang.css.generated";
	//Naming with javax to be able to shared among portlets
	/** Denotes whether JavaScripts are generated for this request. */
	private static final String ATTR_LANG_JS_GENED = "javax.zkoss.zk.lang.js.generated";
	//Naming with javax to be able to shared among portlets
	/** Denotes whether the unavailable message is generated for this request. */
	private static final String ATTR_UNAVAILABLE_GENED = "javax.zkoss.zk.unavail.generated";
	/** Denotes whether zkdt has been generated. */
	private static final String ATTR_DESKTOP_JS_GENED = "javax.zkoss.zk.dtjs.generated";

	/** Denotes DOCTYPE has been generated. */
	private static final String DOCTYPE_GENED = "javax.zkoss.zk.doctype.generated";
	/** Denotes the first line has been generated. */
	private static final String FIRST_LINE_GENED = "javax.zkoss.zk.firstline.generated";

	/** The render context. */
	private static final String ATTR_RENDER_CONTEXT = "org.zkoss.zk.ui.renderContext";
	/** Whether is allowed to generate content directly. */
	private static final String ATTR_DIRECT_CONTENT = "org.zkoss.zk.ui.directContent";
	/** Enabled client info */
	private static final String ATTR_DESKTOP_CLIENTINFO = "org.zkoss.desktop.clientinfo.enabled";
	/** Enabled visibility change */
	private static final String ATTR_DESKTOP_VISIBILITYCHANGE = "org.zkoss.desktop.visibilitychange.enabled";
	/** Support Portlet 2.0 */
	private static final String ATTR_PORTLET2_RESOURCEURL = "org.zkoss.portlet2.resourceURL";
	private static final String ATTR_PORTLET2_NAMESPACE = "org.zkoss.portlet2.namespace";

	/**
	* Set this library property to false to hide the zk version info.
	* @since 6.5.5
	*/
	private static final String ZK_VERSION_INFO_ENABLED_KEY = "org.zkoss.zk.ui.versionInfo.enabled";

	/** Support for Csp tag pattern */
	private static final Pattern SCRIPT_TAG_PATTERN = Pattern.compile("<script(?!\\s+nonce=)");
	private static final Pattern STYLE_TAG_PATTERN = Pattern.compile("<style(?!\\s+nonce=)");

	private static volatile int msgCode = -1;

	/** Sets the content type to the specified execution for the given page.
	 * @param exec the execution (never null)
	 */
	public static final void setContentType(Execution exec, Page page) {
		String contentType = ((PageCtrl) page).getContentType();
		if (contentType == null) {
			contentType = page.getDesktop().getDevice().getContentType();
			if (contentType == null)
				contentType = "";
		}

		final int j = contentType.indexOf(';');
		if (j < 0) {
			final String cs = page.getDesktop().getWebApp().getConfiguration().getResponseCharset();
			if (cs != null && cs.length() > 0)
				contentType += ";charset=" + cs;
		}

		((ExecutionCtrl) exec).setContentType(contentType);
	}

	/** Returns the doc type, or null if not available.
	 * It is null or &lt;!DOCTYPE ...&gt;.
	 */
	public static final String outDocType(Execution exec, Page page) {
		if (exec.getAttribute(DOCTYPE_GENED) == null && !exec.isAsyncUpdate(null)) {
			exec.setAttribute(DOCTYPE_GENED, Boolean.TRUE);
			final String docType = ((PageCtrl) page).getDocType();
			return trimAndLF(docType != null ? docType : page.getDesktop().getDevice().getDocType());
		}
		return "";
	}

	/** Trims and appends a linefeed if necessary.
	 */
	private static final String trimAndLF(String s) {
		if (s != null) {
			s = s.trim();
			final int len = s.length();
			if (len > 0 && s.charAt(len - 1) != '\n')
				s += '\n';
		}
		return s;
	}

	/** Generates the unavailable message in HTML tags, if any.
	 * @param exec the execution (never null)
	 */
	public static String outUnavailable(Execution exec) {
		if (exec.getAttribute(ATTR_UNAVAILABLE_GENED) == null && !exec.isAsyncUpdate(null)) {
			exec.setAttribute(ATTR_UNAVAILABLE_GENED, Boolean.TRUE);

			final Device device = exec.getDesktop().getDevice();
			String s = device.getUnavailableMessage();
			return s != null ? "<noscript>\n" + s + "\n</noscript>" : "";
		}
		return ""; //nothing to generate
	}

	/** Returns the first line to be generated to the output,
	 * or null if no special first line.
	 */
	public static final String outFirstLine(Execution exec, Page page) {
		if (exec.getAttribute(FIRST_LINE_GENED) == null && !exec.isAsyncUpdate(null)) {
			exec.setAttribute(FIRST_LINE_GENED, Boolean.TRUE);
			return trimAndLF(((PageCtrl) page).getFirstLine());
		}
		return "";
	}

	/** Generates the AU responses that are part of a page rendering.
	 * Notice that {@link #outPageContent} will invoke this method automatically.
	 * <p>It is the same as <code>outResponseJavaScripts(exec, false)</code>.
	 */
	public static final String outResponseJavaScripts(Execution exec) {
		return outResponseJavaScripts(exec, false);
	}

	/** Generates the AU responses that are part of a page rendering.
	 * Notice that {@link #outPageContent} will invoke this method automatically.
	 * @param directJS whether to generate JS directly.
	 * If true, it generates <code>"x,y"</code> where x and y are assumed to the responses.
	 * If false, it generates <code>&lt;script&gt;zkac(x,y);&lt;/script&gt;</pre></code>
	 * @since 5.0.2
	 */
	public static final String outResponseJavaScripts(Execution exec, boolean directJS) {
		final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
		final Collection<AuResponse> responses = execCtrl.getResponses();
		final Desktop desktop = exec.getDesktop();
		Configuration config = desktop.getWebApp().getConfiguration();
		if (responses == null || responses.isEmpty())
			return "";
		execCtrl.setResponses(null);

		final StringBuffer sb = new StringBuffer(256);
		if (!directJS)
			sb.append(outCspNonceAttr("<script class=\"z-runonce\" type=\"text/javascript\">\nzkac("));

		for (Iterator<AuResponse> it = responses.iterator(); it.hasNext();) {
			final AuResponse response = it.next();
			sb.append('\'').append(response.getCommand()).append("',");
			final List<?> encdata = response.getEncodedData();
			if (encdata != null)
				sb.append('\'').append(Encode.forJavaScript(org.zkoss.json.JSONArray.toJSONString(encdata)))
						.append('\'');
			else
				sb.append((String) null);
			if (it.hasNext())
				sb.append(",\n");
		}

		if (!directJS)
			sb.append(");\n</script>");
		return sb.toString();
	}

	/** Returns HTML tags to include all JavaScript files and codes that are
	 * required when loading a ZUML page (never null).
	 *
	 * <p>FUTURE CONSIDERATION: we might generate the inclusion on demand
	 * instead of all at once.
	 * @param exec the execution (never null)
	 * @param wapp the Web application.
	 * If null, exec.getDesktop().getWebApp() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution, such as JSP/DSP).
	 * @param deviceType the device type, such as ajax.
	 * If null, exec.getDesktop().getDeviceType() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution).
	 */
	public static final String outLangJavaScripts(Execution exec, WebApp wapp, String deviceType) {
		if (exec.isAsyncUpdate(null) || exec.getAttribute(ATTR_LANG_JS_GENED) != null)
			return ""; //nothing to generate
		exec.setAttribute(ATTR_LANG_JS_GENED, Boolean.TRUE);

		final Desktop desktop = exec.getDesktop();
		if (wapp == null)
			wapp = desktop.getWebApp();
		if (deviceType == null)
			deviceType = desktop != null ? desktop.getDeviceType() : "ajax";

		final StringBuffer sb = new StringBuffer(1536);

		Set<JavaScript> jses = new LinkedHashSet<JavaScript>(32);
		for (LanguageDefinition langdef : LanguageDefinition.getByDeviceType(deviceType))
			jses.addAll(langdef.getJavaScripts());
		if (wapp.getConfiguration().isSourceMapEnabled()) { // use divided javascript
			jses = (Set<JavaScript>) wapp.getAttribute(WpdExtendlet.SOURCE_MAP_JAVASCRIPT_PATH);
		}
		for (JavaScript js : jses)
			append(sb, js);

		// F65-ZK-2061: Check if user want to show or hide zk version info.
		if ("true".equals(Library.getProperty(ZK_VERSION_INFO_ENABLED_KEY, "true"))) {
			sb.append("\n<!-- ZK ").append(wapp.getVersion());
			if (WebApps.getFeature("ee"))
				sb.append(" EE");
			else if (WebApps.getFeature("pe"))
				sb.append(" PE");
			sb.append(' ').append(wapp.getBuild());
			Object o = wapp.getAttribute("org.zkoss.zk.ui.notice");
			if (o != null)
				sb.append(o);
			sb.append(" -->\n");
		}

		int tmout = 0;
		final Boolean autoTimeout = getAutomaticTimeout(desktop);
		if (autoTimeout != null ? autoTimeout.booleanValue() : wapp.getConfiguration().isAutomaticTimeout(deviceType)) {
			if (desktop != null) {
				tmout = desktop.getSession().getMaxInactiveInterval();
			} else {
				Object req = exec.getNativeRequest();
				if (req instanceof HttpServletRequest) {
					final HttpSession hsess = ((HttpServletRequest) req).getSession(false);
					if (hsess != null) {
						final Session sess = SessionsCtrl.getSession(wapp, hsess);
						if (sess != null) {
							tmout = sess.getMaxInactiveInterval();
						} else {
							//try configuration first since HttpSession's timeout is set
							//when ZK Session is created (so it is not set yet)
							//Note: no need to setMaxInactiveInternval here since it will
							//be set later or not useful at the end
							tmout = wapp.getConfiguration().getSessionMaxInactiveInterval();
							if (tmout <= 0) //system default
								tmout = hsess.getMaxInactiveInterval();
						}
					} else
						tmout = wapp.getConfiguration().getSessionMaxInactiveInterval();
				}
			}
			if (tmout > 0) { //unit: seconds
				int extra = tmout / 8;
				tmout += extra > 60 ? 60 : extra < 5 ? 5 : extra;
				//Add extra seconds to ensure it is really timeout
			}
		}

		final boolean keepDesktop = exec.getAttribute(Attributes.NO_CACHE) == null
				&& !"page".equals(ExecutionsCtrl.getPageRedrawControl(exec)),
				groupingAllowed = isGroupingAllowed(desktop);
		final String progressboxPos = org.zkoss.lang.Library.getProperty("org.zkoss.zul.progressbox.position", "");
		if (tmout > 0 || keepDesktop || progressboxPos.length() > 0 || !groupingAllowed) {
			sb.append(outCspNonceAttr("<script class=\"z-runonce\" type=\"text/javascript\">\nzkopt({"));

			if (keepDesktop)
				sb.append("kd:1,");
			if (!groupingAllowed)
				sb.append("gd:1,");
			if (tmout > 0)
				sb.append("to:").append(tmout).append(',');
			if (progressboxPos.length() > 0)
				sb.append("ppos:'").append(progressboxPos).append('\'');

			if (sb.charAt(sb.length() - 1) == ',')
				sb.setLength(sb.length() - 1);
			sb.append("});\n</script>");
		}

		final Device device = Devices.getDevice(deviceType);
		String s = device.getEmbedded();
		if (s != null)
			sb.append(s).append('\n');

		Map<String, DataHandlerInfo> dataHandlers = wapp.getConfiguration().getDataHandlers();
		Set<Pair<String, String>> scripts = new LinkedHashSet<Pair<String, String>>();
		for (DataHandlerInfo info : dataHandlers.values()) {
			List<Pair<String, String>> scripts2 = info.getScripts();
			int size;
			if (scripts2 != null && (size = scripts2.size()) > 1)
				scripts.addAll(scripts2.subList(0, size - 1));
		}
		for (Pair<String, String> scriptInfo : scripts) {
			String src = scriptInfo.getX();
			sb.append("<script type=\"text/javascript\"");
			if (src != null && src.length() != 0)
				sb.append(" src=\"").append(Encode.forHtmlAttribute(Executions.encodeURL(src))).append("\" charset=\"UTF-8\">");
			else
				sb.append(">").append(scriptInfo.getY());
			sb.append("</script>\n");
		}
		return outCspNonceAttr(sb.toString());
	}

	private static Boolean getAutomaticTimeout(Desktop desktop) {
		if (desktop != null)
			for (Page page : desktop.getPages()) {
				Boolean b = ((PageCtrl) page).getAutomaticTimeout();
				if (b != null)
					return b;
			}
		return null;
	}

	/** Returns HTML tags to include all style sheets that are
	 * defined in all languages of the specified device (never null).
	 *
	 * <p>In addition to style sheets defined in lang.xml and lang-addon.xml,
	 * it also include:
	 * <ol>
	 * <li>The style sheet specified in the theme-uri parameter.</li>
	 * </ol>
	 *
	 * <p>FUTURE CONSIDERATION: we might generate the inclusion on demand
	 * instead of all at once.
	 * @param exec the execution (never null)
	 * @param wapp the Web application.
	 * If null, exec.getDesktop().getWebApp() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution, such as JSP/DSP).
	 * @param deviceType the device type, such as ajax.
	 * If null, exec.getDesktop().getDeviceType() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution).
	 */
	public static final String outLangStyleSheets(Execution exec, WebApp wapp, String deviceType) {
		if (exec.isAsyncUpdate(null) || exec.getAttribute(ATTR_LANG_CSS_GENED) != null)
			return ""; //nothing to generate
		exec.setAttribute(ATTR_LANG_CSS_GENED, Boolean.TRUE);

		final StringBuffer sb = new StringBuffer(512);
		for (StyleSheet ss : getStyleSheets(exec, wapp, deviceType))
			append(sb, ss, exec, null);

		if (sb.length() > 0)
			sb.append('\n');

		if (wapp == null)
			wapp = exec.getDesktop().getWebApp();
		Map<String, DataHandlerInfo> dataHandlers = wapp.getConfiguration().getDataHandlers();
		Set<Map<String, String>> links = new LinkedHashSet<Map<String, String>>();
		for (DataHandlerInfo info : dataHandlers.values()) {
			List<Map<String, String>> links2 = info.getLinks();
			if (links2 != null)
				links.addAll(links2);
		}
		for (Map<String, String> info : links) {
			sb.append("\n<link");
			for (Map.Entry<String, String> entry : info.entrySet()) {
				if ("href".equals(entry.getKey()))
					HTMLs.appendAttribute(sb, entry.getKey(), Executions.encodeURL(entry.getValue()), true);
				else
					HTMLs.appendAttribute(sb, entry.getKey(), entry.getValue(), true);
			}
			sb.append("/>");
		}
		return outCspNonceAttr(sb.toString());
	}

	/** Returns a list of {@link StyleSheet} that shall be generated
	 * to the client for the specified execution.
	 * @param exec the execution (never null)
	 * @param wapp the Web application.
	 * If null, exec.getDesktop().getWebApp() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution, such as JSP/DSP).
	 * @param deviceType the device type, such as ajax.
	 * If null, exec.getDesktop().getDeviceType() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution).
	 */
	public static final List<StyleSheet> getStyleSheets(Execution exec, WebApp wapp, String deviceType) {
		if (wapp == null)
			wapp = exec.getDesktop().getWebApp();
		if (deviceType == null)
			deviceType = exec.getDesktop().getDeviceType();

		final Configuration config = wapp.getConfiguration();
		final Set<String> disabled = config.getDisabledThemeURIs();
		final List<StyleSheet> sses = new LinkedList<StyleSheet>(); //a list of StyleSheet
		for (LanguageDefinition langdef : LanguageDefinition.getByDeviceType(deviceType)) {
			for (StyleSheet ss : langdef.getStyleSheets()) {
				if (!disabled.contains(ss.getHref()))
					sses.add(ss);
			}
		}

		// ZK-4771: Provide a way to add theme-uri without extends any existing ThemeProvider
		final ThemeURIModifier modifier = new SimpleThemeURIModifier(sses);
		final List<ThemeURIHandler> themeURIHandlers = config.getThemeURIHandlers();
		for (ThemeURIHandler themeURIHandler : themeURIHandlers) {
			themeURIHandler.modifyThemeURIs(exec, modifier);
		}

		//Process configuration
		final ThemeProvider themeProvider = config.getThemeProvider();
		if (themeProvider != null) {
			final List<Object> orgss = new LinkedList<Object>();
			for (StyleSheet ss : sses) {
				final String href = ss.getHref();
				if (href != null && href.length() > 0)
					orgss.add((ss.getMedia() != null || ss.isDisabled()) ? ss : href); //we don't support getContent
			}

			final String[] hrefs = config.getThemeURIs();
			for (int j = 0; j < hrefs.length; ++j)
				orgss.add(hrefs[j]);

			sses.clear();
			final Collection<?> res = themeProvider.getThemeURIs(exec, orgss);
			if (res != null) {
				for (Object re : res) {
					sses.add(re instanceof StyleSheet ? (StyleSheet) re : new StyleSheet((String) re, "text/css"));
				}
			}
		} else {
			final String[] hrefs = config.getThemeURIs();
			for (int j = 0; j < hrefs.length; ++j)
				sses.add(new StyleSheet(hrefs[j], "text/css"));
		}
		return sses;
	}

	private static void append(StringBuffer sb, StyleSheet ss, Execution exec, Page page) {
		String href = ss.getHref();
		String media = ss.getMedia();
		if (href != null) {
			try {
				if (exec != null)
					href = (String) exec.evaluate(page, href, String.class);

				if (href != null && href.length() > 0) {
					sb.append("\n<link rel=\"stylesheet\" type=\"").append(ss.getType()).append("\" href=\"")
							.append(Encode.forHtmlAttribute(ServletFns.encodeURL(href)));
					if (media != null)
						sb.append("\" media=\"").append(media);
					sb.append("\"");
					if (ss.isDisabled())
						sb.append(" disabled");
					sb.append("/>");
				}
			} catch (javax.servlet.ServletException ex) {
				throw new UiException(ex);
			}
		} else {
			sb.append("\n<style");
			if (ss.getType() != null)
				sb.append(" type=\"").append(ss.getType()).append('"');
			sb.append(">\n").append(ss.getContent()).append("\n</style>");
		}
	}

	private static void append(StringBuffer sb, JavaScript js) {
		sb.append("\n<script type=\"text/javascript\"");
		if (js.getSrc() != null) {
			String url;
			try {
				url = ServletFns.encodeURL(js.getSrc());
			} catch (javax.servlet.ServletException ex) {
				throw new UiException(ex);
			}

			sb.append(" src=\"").append(Encode.forHtmlAttribute(url)).append('"');
			final String charset = js.getCharset();
			if (charset != null)
				sb.append(" charset=\"").append(charset).append('"');
			sb.append('>');
		} else {
			sb.append(" class=\"z-runonce\">\n")
					// B65-ZK-1836
					.append(js.getContent().replaceAll("</(?i)(?=script>)", "<\\\\/")).append("\n");
		}
		sb.append("</script>");
	}

	/** Returns the render context, or null if not available.
	 * It is used to render the content that will appear before the content
	 * generated by {@link ContentRenderer}, such as crawlable content.
	 *
	 * @param exec the execution. If null, {@link Executions#getCurrent}
	 * is assumed.
	 */
	public static final RenderContext getRenderContext(Execution exec) {
		if (exec == null)
			exec = Executions.getCurrent();
		return exec != null ? (RenderContext) exec.getAttribute(ATTR_RENDER_CONTEXT) : null;
	}

	private static final void setRenderContext(Execution exec, RenderContext rc) {
		exec.setAttribute(ATTR_RENDER_CONTEXT, rc);
	}

	/** Returns the HTML content representing a page.
	 * @param au whether it is caused by asynchronous update
	 * @param exec the execution (never null)
	 */
	public static final void outPageContent(Execution exec, Page page, Writer out, boolean au) throws IOException {
		final Desktop desktop = page.getDesktop();
		final PageCtrl pageCtrl = (PageCtrl) page;
		final Component owner = pageCtrl.getOwner();
		boolean contained = owner == null && exec.isIncluded();
		//a standalone page (i.e., no owner), and being included by
		//non-ZK page (e.g., JSP).
		//
		//Revisit Bug 2001707: OK to use exec.isIncluded() since
		//we use PageRenderer now (rather than Servlet's include)
		//TODO: test again

		//prepare style
		String style = page.getStyle();
		if (style == null || style.length() == 0) {
			style = null;
			String wd = null, hgh = null;
			if (owner instanceof HtmlBasedComponent) {
				final HtmlBasedComponent hbc = (HtmlBasedComponent) owner;
				wd = hbc.getWidth(); //null if not set
				hgh = hbc.getHeight(); //null if not set
			}

			if (wd != null || hgh != null || contained) {
				final StringBuffer sb = new StringBuffer(32);
				HTMLs.appendStyle(sb, "width", wd != null ? wd : "100%");
				HTMLs.appendStyle(sb, "height", hgh != null ? hgh : contained ? null : "100%");
				style = sb.toString();
			}
		}

		RenderContext rc = null, old = null;
		final boolean aupg = exec.isAsyncUpdate(page); //AU this page
		final boolean includedAndPart = owner != null && !aupg;
		//this page is included and rendered with its owner
		final boolean divRequired = !au || includedAndPart;
		final boolean standalone = !au && owner == null;
		if (standalone) {
			rc = new RenderContext(out, new StringWriter(), desktop.getWebApp().getConfiguration().isCrawlable(),
					false);
			setRenderContext(exec, rc);
		} else if (owner != null) {
			old = getRenderContext(exec); //store
			final boolean crawlable = old != null && old.temp != null
					&& desktop.getWebApp().getConfiguration().isCrawlable();
			setRenderContext(exec, crawlable ? new RenderContext(old.temp, null, true, true) : null);
		}

		//generate div first
		if (divRequired) {
			outDivTemplateBegin(out, page.getUuid());
		}
		if (standalone) { //switch out
			//don't call outDivTemplateEnd yet since rc.temp will be generated before it
			out = new StringWriter();
		} else if (divRequired) {
			outDivTemplateEnd(page, out); //close it now since no rc.temp
		}

		if (includedAndPart) {
			out = new StringWriter();
		} else if (divRequired) {
			//generate JS second
			out.write("\n<script class=\"z-runonce\" type=\"text/javascript\">\n");
		}

		exec.setAttribute(ATTR_DESKTOP_JS_GENED, Boolean.TRUE);
		final int order = ComponentRedraws.beforeRedraw(false);
		final String extra;
		try {
			if (order < 0) {
				if (aupg)
					out.write('[');
				else {
					out.write(outSpecialJS(desktop));

					// zkdh should run before zkmx()
					WebApp webApp = WebApps.getCurrent();
					Configuration configuration = webApp.getConfiguration();
					Map<String, DataHandlerInfo> dataHandlers = configuration.getDataHandlers();
					for (Map.Entry<String, DataHandlerInfo> me : dataHandlers.entrySet()) {
						DataHandlerInfo handler = me.getValue();
						List<Pair<String, String>> scripts = handler.getScripts();
						Pair<String, String> scriptInfo = scripts.get(scripts.size() - 1);
						String scriptSrc = scriptInfo.getX();
						String script = scriptInfo.getY();
						if (scriptSrc != null && scriptSrc.length() != 0) {
							script = Devices.loadJavaScript(exec, scriptSrc);
						}
						out.write("zkdh('" + me.getKey() + "', " + script + ");\n");
					}
					out.write("zk.afterLoad(function(){");
					out.write(divRequired ? "zkmx(" : "zkx(");
				}
			} else if (order > 0) //not first child
				out.write(',');
			out.write("\n[0,'"); //0: page
			out.write(page.getUuid());
			out.write("',{");

			final StringBuffer props = new StringBuffer(128);
			final String pgid = page.getId();
			if (pgid.length() > 0)
				appendProp(props, "id", pgid);
			if (owner != null) {
				appendProp(props, "ow", owner.getUuid());
			} else {
				appendProp(props, "dt", desktop.getId());
				appendProp(props, "cu", getContextURI(exec));
				appendProp(props, "uu",  desktop.getUpdateURI(null));
				appendProp(props, "rsu", desktop.getResourceURI(null));
				appendProp(props, "ru", desktop.getRequestPath());
			}
			final String pageWgtCls = pageCtrl.getWidgetClass();
			if (pageWgtCls != null)
				appendProp(props, "wc", pageWgtCls);
			if (style != null)
				appendProp(props, "style", style);
			if (!isClientROD(page))
				appendProp(props, "z$rod", Boolean.FALSE);
			if (contained)
				appendProp(props, "ct", Boolean.TRUE);
			out.write(props.toString());
			out.write("},{"); // preserved for shadow element
			out.write("},[");

			for (Component root = page.getFirstRoot(); root != null; root = root.getNextSibling())
				((ComponentCtrl) root).redraw(out);

			out.write("]]");
		} finally {
			extra = ComponentRedraws.afterRedraw();
		}

		if (order < 0) {
			outEndJavaScriptFunc(exec, out, extra, aupg, true);
		}

		if (standalone) {
			setRenderContext(exec, null);

			StringBuffer sw = ((StringWriter) out).getBuffer();
			out = rc.temp;
			if (divRequired)
				outDivTemplateEnd(page, out);
			//close tag after temp, but before perm (so perm won't be destroyed)
			Files.write(out, ((StringWriter) rc.perm).getBuffer()); //perm

			// B65-ZK-1836
			Files.write(out, new StringBuffer(outCspNonceAttr(sw.toString().replaceAll("</(?i)(?=script>)", "<\\\\/")))); //js
		} else if (owner != null) { //restore
			setRenderContext(exec, old);
		}

		if (includedAndPart) {
			((Includer) owner).setRenderingResult(((StringWriter) out).toString());
		} else if (divRequired) {
			out.write("\n</script>\n");
		}
	}

	private static void outDivTemplateBegin(Writer out, String uuid) throws IOException {
		out.write("<div");
		writeAttr(out, "id", uuid);
		out.write(" class=\"z-temp\"><div id=\"zk_proc\" class=\"z-loading\"><div class=\"z-loading-indicator\">"
				// B65-ZK-1852: Displays localized loading label.
				+ "<span class=\"z-loading-icon\"></span>" + Encode.forHtmlContent(getLoadingLabel()) + "</div></div>");
	}

	private static String getLoadingLabel() {
		if (msgCode == -1) {
			try {
				Class<?> msgClass = Classes.forNameByThread("org.zkoss.zul.mesg.MZul");
				Field msgField = msgClass.getField("PLEASE_WAIT");
				msgCode = msgField.getInt(null);
			} catch (Throwable ex) {
				return "Processing...";
			}
		}
		// B70-ZK-1852: Returns the localized label.
		return msgCode != -1 ? Messages.get(msgCode) : "Processing...";
	}

	private static void outDivTemplateEnd(Page page, Writer out) throws IOException {
		final Desktop dt;
		if (page != null && (dt = page.getDesktop()) != null) {
			if (dt.getAttribute(ATTR_DESKTOP_CLIENTINFO) != null) {
				dt.removeAttribute(ATTR_DESKTOP_CLIENTINFO);
				if (!"CE".equals(WebApps.getEdition()))
					out.write(
							"<script type=\"text/javascript\">if(zk.clientinfo === undefined)zk.clientinfo = true;</script>");
			}
			if (dt.getAttribute(ATTR_DESKTOP_VISIBILITYCHANGE) != null) {
				dt.removeAttribute(ATTR_DESKTOP_VISIBILITYCHANGE);
				out.write(
						"<script type=\"text/javascript\">if(zk.visibilitychange === undefined)zk.visibilitychange = true;</script>");
			}
			String resourceURL = (String) page.getAttribute(ATTR_PORTLET2_RESOURCEURL, Page.PAGE_SCOPE),
					namespace = (String) page.getAttribute(ATTR_PORTLET2_NAMESPACE, Page.PAGE_SCOPE);
			if (resourceURL != null) {
				page.removeAttribute(ATTR_PORTLET2_RESOURCEURL, Page.PAGE_SCOPE);
				page.removeAttribute(ATTR_PORTLET2_NAMESPACE, Page.PAGE_SCOPE);
				// B65-ZK-2210: store url and namespace per desktop.
				out.write("<script type=\"text/javascript\">if(!zk.portlet2Data) zk.portlet2Data = {};\n"
						+ "zk.portlet2Data['" + dt.getId() + "'] = {" + "resourceURL: '" + resourceURL + "', "
						+ "namespace: '" + namespace + "'};</script>");
			}
		}
		outSEOContent(page, out);
		out.write("</div>");
	}

	protected static String outCspNonceAttr(String output) {
		Configuration config = WebApps.getCurrent().getConfiguration();
		boolean cspEnabled = config.isCspEnabled(),
				cspStrictDynamicEnabled = config.isCspStrictDynamicEnabled();

		if (cspEnabled && cspStrictDynamicEnabled && output != null) {
			String nonce = config.getCspProvider().getCspNonce();
			String nonceAttr = " nonce=\"" + Encode.forHtmlAttribute(nonce) + "\"";

			String result = SCRIPT_TAG_PATTERN.matcher(output).replaceAll("<script" + nonceAttr);
			result = STYLE_TAG_PATTERN.matcher(result).replaceAll("<style" + nonceAttr);
			return result;
		}
		return output;
	}

	/** Generates the SEO content for the given page.
	 * Nothing is generated if the SEO content has been generated or it shall not be generated.
	 * <p>The SEO content shall be placed inside <code>&lt;div class="z-temp"&gt;</code>.
	 * <p>If a page renderer generates <code>&lt;div class="z-temp"&gt;</code> by itself,
	 * it must invoke this method.
	 * @since 5.0.9
	 */
	public static void outSEOContent(Page page, Writer out) throws IOException {
		if (page != null && ((PageCtrl) page).getOwner() == null) { //only the topmost page shall generate SEO
			final SEORenderer[] sds = page.getDesktop().getWebApp().getConfiguration().getSEORenderers();
			for (int j = 0; j < sds.length; ++j)
				sds[j].render(page, out);
		}
	}

	/** Generates end of the function (of zkx).
	 * It assumes the function name and the first parenthesis has been generated.
	 * @param aupg whether the current page is caused by AU request
	 */
	private static void outEndJavaScriptFunc(Execution exec, Writer out, String extra, boolean aupg, boolean afterLoad)
			throws IOException {
		final String ac = outResponseJavaScripts(exec, true);
		if (aupg) {
			if (extra.length() > 0 || ac.length() > 0) {
				out.write(",0,"); //no need to delay since js is evaluated by zkx

				if (ac.length() > 0) {
					out.write("\n[");
					out.write(ac);
					out.write(']');
				} else {
					out.write("null");
				}

				if (extra.length() > 0) {
					out.write(",'");
					out.write(Encode.forJavaScript(extra));
					out.write('\'');
				}
			}
			out.write(']');
		} else {
			if (extra.length() > 0 || ac.length() > 0) {
				out.write(',');
				out.write(extra.length() > 0 ? '9' : '0');
				//Bug 2983792: delay until non-defer script (i.e., extra) evaluated

				if (ac.length() > 0) {
					out.write(",\n[");
					out.write(ac);
					out.write(']');
				}
			}
			out.write(");\n");
			if (afterLoad) {
				out.write("});");
			}
			out.write(extra);
		}
	}

	private static void appendProp(StringBuffer sb, String name, Object value) {
		if (sb.length() > 0)
			sb.append(',');
		sb.append(name).append(':');
		boolean quote = value instanceof String;
		if (quote) sb.append('\'');
		sb.append(Encode.forJavaScript(String.valueOf(value)));
		if (quote) sb.append('\'');
	}

	/** Generates the special JavaScript code, such as the application's name.
	 * It shall be called, before generating "zkmx(" and "zkx(".
	 * @since 5.0.6
	 */
	public static final String outSpecialJS(Desktop desktop) {
		final StringBuffer sb = new StringBuffer();

		//output application name
		String oldnm = (String) desktop.getAttribute(ATTR_APPNM);
		if (oldnm == null)
			oldnm = "ZK";
		final String appnm = desktop.getWebApp().getAppName();
		if (!oldnm.equals(appnm)) {
			sb.append("zk.appName='");
			sb.append(Encode.forJavaScript(appnm)).append("';");
			desktop.setAttribute(ATTR_APPNM, appnm);
		}

		//output zktheme cookie
		String themenm = ThemeFns.getCurrentTheme();
		sb.append("zk.themeName='");
		sb.append(Encode.forJavaScript(themenm)).append("';");
		desktop.setAttribute(ATTR_THEMENM, themenm);

		//output ZK ICON
		final Session sess = Sessions.getCurrent();
		if (sess != null) {
			WebApp wapp = desktop.getWebApp();
			if (wapp == null || "CE".equals(WebApps.getEdition())
					|| wapp.getAttribute("org.zkoss.zk.ui.notice") != null) {
				final PI pi = (PI) sess.getAttribute(ATTR_PI);
				boolean show = pi == null;
				if (show)
					sess.setAttribute(ATTR_PI, new PI());
				else
					show = pi.show();
				if (show)
					sb.append("zk.pi=1;");
			}
		}
		return sb.toString();
	}

	private static final String ATTR_APPNM = "org.zkoss.zk.appnm";
	private static final String ATTR_THEMENM = "org.zkoss.zk.zkthemenm";
	private static final String ATTR_PI = "org.zkoss.zk.pi";

	private static class PI implements java.io.Serializable {
		long _t;

		private PI() {
			_t = System.currentTimeMillis();
		}

		private boolean show() {
			long now = System.currentTimeMillis();
			if (now - _t > 600000) { //every 10 minutes
				_t = now;
				return true;
			}
			return false;
		}
	}

	private static final boolean isClientROD(Page page) {
		Object o = page.getAttribute(Attributes.CLIENT_ROD);
		if (o != null)
			return (o instanceof Boolean && ((Boolean) o).booleanValue()) || !"false".equals(o);

		if (_crod == null) {
			final String s = Library.getProperty(Attributes.CLIENT_ROD);
			_crod = Boolean.valueOf(s == null || !"false".equals(s));
		}
		return _crod.booleanValue();
	}

	private static Boolean _crod;

	private static final boolean isGroupingAllowed(Desktop desktop) {
		final String name = "org.zkoss.zk.ui.input.grouping.allowed";
		if (desktop != null) {
			final Collection<Page> pages = desktop.getPages();
			if (!pages.isEmpty()) {
				final Page page = pages.iterator().next();
				Object o = page.getAttribute(name);
				if (o != null)
					return (o instanceof Boolean && ((Boolean) o).booleanValue()) || !"false".equals(o);
			}
		}

		if (_groupingAllowed == null) {
			final String s = Library.getProperty(name);
			_groupingAllowed = Boolean.valueOf(s == null || !"false".equals(s));
		}
		return _groupingAllowed.booleanValue();
	}

	private static Boolean _groupingAllowed;

	/** Used to indicate ZK Data Handler Scripts are generated. */
	private static final String ATTR_ZK_DATAHANDLER_SCRIPTS_GENERATED = "zkDataHandlerScriptsGenerated";

	/** Generates the content of a standalone component that
	 * the peer widget is not a child of the page widget at the client.
	 * @param comp the component to render. It is null if no child component
	 * at all.
	 */
	public static final void outStandalone(Execution exec, Component comp, Writer out) throws IOException {
		if (ComponentRedraws.beforeRedraw(false) >= 0)
			throw new InternalError("Not possible: " + comp);

		final String extra;
		try {
			if (comp != null) {
				outDivTemplateBegin(out, comp.getUuid());
				outDivTemplateEnd(comp.getPage(), out);
			}

			out.write("<script class=\"z-runonce\" type=\"text/javascript\">\n");

			// zkdh should run before zkmx()
			WebApp webApp = WebApps.getCurrent();
			Configuration configuration = webApp.getConfiguration();

			if (exec.getAttribute(ATTR_ZK_DATAHANDLER_SCRIPTS_GENERATED) == null) {
				Map<String, DataHandlerInfo> dataHandlers = configuration.getDataHandlers();
				for (Map.Entry<String, DataHandlerInfo> me : dataHandlers.entrySet()) {
					DataHandlerInfo handler = me.getValue();
					List<Pair<String, String>> scripts = handler.getScripts();
					Pair<String, String> scriptInfo = scripts.get(scripts.size() - 1);
					String scriptSrc = scriptInfo.getX();
					String script = scriptInfo.getY();
					if (scriptSrc != null && scriptSrc.length() != 0)
						script = Devices.loadJavaScript(exec, scriptSrc);
					out.write("zkdh('" + me.getKey() + "', " + script + ");\n");
				}
				exec.setAttribute(ATTR_ZK_DATAHANDLER_SCRIPTS_GENERATED, true);
			}

			out.write("\nzkmx(");
			if (comp != null)
				((ComponentCtrl) comp).redraw(out);
			else
				out.write("null"); //no component at all
		} finally {
			extra = ComponentRedraws.afterRedraw();
		}

		outEndJavaScriptFunc(exec, out, extra, false, false);
		//generate extra, responses and ");"
		out.write("\n</script>\n");
	}

	private static final void writeAttr(Writer out, String name, String value) throws IOException {
		out.write(' ');
		out.write(name);
		out.write("=\"");
		out.write(Encode.forHtmlAttribute(value));
		out.write('"');
	}

	/** Returns the content of the specified condition
	 * that will be placed inside the header element of the specified page,
	 * or null if it was generated before.
	 * For HTML, the header element is the HEAD element.
	 *
	 * <p>Notice that this method ignores the following invocations
	 * against the same page in the same execution. In other words,
	 * it is safe to invoke this method multiple times.
	 *
	 * @param before whether to return the headers that shall be shown
	 * before ZK's CSS/JS headers.
	 * If true, only the headers that shall be shown before (such as meta)
	 * are returned.
	 * If true, only the headers that shall be shown after (such as link)
	 * are returned.
	 */
	public static final String outHeaders(Execution exec, Page page, boolean before) {
		if (page == null)
			return "";

		String attr = "zkHeaderGened" + page.getUuid();
		if (before)
			attr += "Bf";
		if (exec.getAttribute(attr) != null)
			return null;

		exec.setAttribute(attr, Boolean.TRUE); //generated only once
		return outCspNonceAttr(before ? ((PageCtrl) page).getBeforeHeadTags() : ((PageCtrl) page).getAfterHeadTags());
	}

	/** Generates and returns the ZK specific HTML tags including
	 * the headers defined in the specified page, or null if it was
	 * generated before.
	 *
	 * <p>It is shortcut of<br/>
	 *<code>outZkHeader(exec, page, true)+outZkTags(exec, null, null)+outZkHeader(exec, page, false)</code>
	 *
	 * <p>Unlike {@link #outZkTags}, this method cannot be called
	 * in JSP/DSP (since desktop is not available).
	 *
	 * @see #outZkTags
	 */
	public static String outHeaderZkTags(Execution exec, Page page) {
		String s1 = outHeaders(exec, page, true), s2 = outZkTags(exec, null, null), s3 = outHeaders(exec, page, false);
		return s1 != null ? s2 != null ? s3 != null ? s1 + s2 + s3 : s1 + s2 : s3 != null ? s1 + s3 : s1
				: //s2 null
				s2 != null ? s3 != null ? s2 + s3 : s2 : s3 != null ? s3 : null; //s2 null
	}

	/** Generates and returns the ZK specific HTML tags such as stylesheet
	 * and JavaScript.
	 *
	 * <p>For each desktop, we have to generate a set of HTML tags
	 * to load ZK Client engine, style sheets and so on.
	 * For ZUL pages, it is generated automatically by page.dsp.
	 * However, for ZHTML pages, we have to generate these tags
	 * with special component such as org.zkoss.zhtml.Head, such that
	 * the result HTML page is legal.
	 *
	 * @param exec the execution (never null)
	 * @return the string holding the HTML tags, or null if already generated.
	 * @param wapp the Web application.
	 * If null, exec.getDesktop().getWebApp() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution, such as JSP/DSP).
	 * @param deviceType the device type, such as ajax.
	 * If null, exec.getDesktop().getDeviceType() is used.
	 * So you have to specify it if the execution is not associated
	 * with desktop (a fake execution).
	 * @see #outHeaderZkTags
	 */
	public static String outZkTags(Execution exec, WebApp wapp, String deviceType) {
		if (exec.getAttribute(ATTR_ZK_TAGS_GENERATED) != null)
			return null;
		exec.setAttribute(ATTR_ZK_TAGS_GENERATED, Boolean.TRUE);

		final StringBuffer sb = new StringBuffer(512).append('\n').append(outLangJavaScripts(exec, wapp, deviceType))
				.append(outLangStyleSheets(exec, wapp, deviceType));

		final Desktop desktop = exec.getDesktop();
		if (desktop != null && exec.getAttribute(ATTR_DESKTOP_JS_GENED) == null) {
			sb.append("<script class=\"z-runonce\" type=\"text/javascript\">\nzkdt('").append(desktop.getId())
					.append("','").append(Encode.forJavaScript(getContextURI(exec))).append("','").append(Encode.forJavaScript(desktop.getUpdateURI(null)))
					.append("','").append(Encode.forJavaScript(desktop.getResourceURI(null)))
					.append("','").append(Encode.forJavaScript(desktop.getRequestPath())).append("');").append(outSpecialJS(desktop))
					.append("\n</script>\n");
		}

		// Bug ZK-2649
		if ("complete".equals(exec.getAttribute(Attributes.PAGE_REDRAW_CONTROL))) {
			final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
			final Collection responses = execCtrl.getResponses();
			if (responses != null && !responses.isEmpty()) {
				execCtrl.setResponses(null);
				sb.append("\n<script>zk.afterMount(function(){\n");
				for (Iterator it = responses.iterator(); it.hasNext();) {
					final AuResponse response = (AuResponse) it.next();
					sb.append("zAu.process('").append(response.getCommand()).append("'");

					final List encdata = response.getEncodedData();
					if (encdata != null)
						sb.append(",'").append(Encode.forJavaScript(org.zkoss.json.JSONArray.toJSONString(encdata))).append('\'');
					sb.append(");\n");
				}
				sb.append("});\n</script>\n");
			}
		}

		return sb.toString();
	}

	/** Returns if the ZK specific HTML tags are generated.
	 * @since 5.0.3
	 */
	public static boolean isZkTagsGenerated(Execution exec) {
		return exec.getAttribute(ATTR_ZK_TAGS_GENERATED) != null;
	}

	/** Used to indicate ZK tags are generated. */
	private static final String ATTR_ZK_TAGS_GENERATED = "zkHtmlTagsGened";

	private static String getContextURI(Execution exec) {
		if (exec != null) {
			String s = exec.getContextURI();
			int j = s.lastIndexOf('/'); //might have jsessionid=...
			return j >= 0 ? s.substring(0, j) + s.substring(j + 1) : s;
		}
		return "";
	}

	/** Sets whether a component can directly generate HTML tags
	 * to the output.
	 * @see #isDirectContent
	 */
	public static boolean setDirectContent(Execution exec, boolean direct) {
		return (direct ? exec.setAttribute(ATTR_DIRECT_CONTENT, Boolean.TRUE)
				: exec.removeAttribute(ATTR_DIRECT_CONTENT)) != null;
	}

	/** Returns whether a component can directly generate HTML tags
	 * to the output.
	 * This flag is used by components that can generate the content
	 * directly, such as {@link org.zkoss.zk.ui.HtmlNativeComponent}
	 * @see #setDirectContent
	 */
	public static boolean isDirectContent(Execution exec) {
		if (exec == null)
			exec = Executions.getCurrent();
		return exec != null && exec.getAttribute(ATTR_DIRECT_CONTENT) != null;
	}

	/** The render context which consists of two writers ({@link #temp} and
	* {@link #perm}.
	 * @see HtmlPageRenders#getRenderContext
	 */
	public static class RenderContext {
		/** The writer used to generate the content that will
		 * be replaced after the widgets have been rendered.
		 * It is mainly used to put the crawlable content, which
		 * is used only for Search Engine.
		 * <p>It is never null.
		 */
		public final Writer temp;
		/** The writer used to generate the content that exists
		 * even after the widgets have been rendered.
		 * It is currently used only to generate CSS style.
		 * <p>It is null if the current page is included by another.
		 */
		public final Writer perm;
		/** Indicates whether to generate crawlable content.
		 */
		public final boolean crawlable;
		/** Indicated whether this page/execution is included.
		 * If included, the component shall not use zk(x).detachChildren(),
		 * since the page's rendering might be delayed.
		 */
		public final boolean included;

		private RenderContext(Writer temp, Writer perm, boolean crawlable, boolean included) {
			this.temp = temp;
			this.perm = perm;
			if (crawlable && WebApps.getFeature("ee")) {
				this.crawlable = crawlable;
			} else
				this.crawlable = false;
			this.included = included;
		}
	}

	/** Returns Script tag to include init-crash-script and init-crash-timeout defined in zk.xml
	 *
	 * <p>It will be placed before all JavaScript files and codes generated by {@link #outLangJavaScripts}
	 *
	 * @param exec the execution (never null)
	 * @param wapp the Web application.
	 * If null, exec.getDesktop().getWebApp() is used.
	 *
	 * @since 7.0.4
	 */
	public static final String outInitCrashScript(Execution exec, WebApp wapp) {
		if (exec.isAsyncUpdate(null))
			return ""; //nothing to generate

		if (wapp == null)
			wapp = exec.getDesktop().getWebApp();
		Configuration config = wapp.getConfiguration();
		String script = config.getInitCrashScript();
		int timeout = config.getInitCrashTimeout();
		final StringBuffer sb = new StringBuffer(512);
		if (script != null || timeout >= 0) {
			sb.append("<script type=\"text/javascript\">\n");
		}
		if (timeout >= 0) {
			sb.append("window.zkInitCrashTimeout = " + timeout + ";\n");
		}
		if (script != null) {
			sb.append(script + "\n");
		}
		if (script != null || timeout >= 0) {
			sb.append("</script>");
		}
		return outCspNonceAttr(sb.toString());
	}
}
