/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.xforms.processor.handlers.xhtml; import org.apache.commons.lang3.StringUtils; import org.orbeon.dom.QName; import org.orbeon.oxf.resources.ResourceManagerWrapper; import org.orbeon.oxf.xforms.*; import org.orbeon.oxf.xforms.analysis.ElementAnalysis; import org.orbeon.oxf.xforms.analysis.controls.AppearanceTrait$; import org.orbeon.oxf.xforms.control.LHHASupport; import org.orbeon.oxf.xforms.control.XFormsControl$; import org.orbeon.oxf.xforms.control.controls.XFormsRepeatControl; import org.orbeon.oxf.xforms.processor.handlers.HandlerContext; import org.orbeon.oxf.xforms.processor.handlers.NullElementHandler; import org.orbeon.oxf.xforms.processor.handlers.NullHandler; import org.orbeon.oxf.xforms.state.XFormsStateManager; import org.orbeon.oxf.xforms.xbl.XBLBindings; import org.orbeon.oxf.xml.*; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import scala.Option; /** * Handle xhtml:body. */ public class XHTMLBodyHandler extends XFormsBaseHandlerXHTML { private XMLReceiverHelper helper; // private String formattingPrefix; public XHTMLBodyHandler() { super(false, true); } public void start(String uri, String localname, String qName, Attributes attributes) throws SAXException { // Register control handlers on controller registerHandlers(handlerContext.getController(), containingDocument); // Add class for YUI skin attributes = SAXUtils.appendToClassAttribute(attributes, "yui-skin-sam"); // Handle AVTs attributes = handleAVTsAndIDs(attributes, XHTMLElementHandler.REF_ID_ATTRIBUTE_NAMES); // Start xhtml:body final XMLReceiver xmlReceiver = handlerContext.getController().getOutput(); xmlReceiver.startElement(uri, localname, qName, attributes); helper = new XMLReceiverHelper(xmlReceiver); final String htmlPrefix = XMLUtils.prefixFromQName(qName); // Get formatting prefix and declare it if needed // TODO: would be nice to do this here, but then we need to make sure this prefix is available to other handlers // formattingPrefix = handlerContext.findFormattingPrefixDeclare(); final boolean isEmbeddedClient = containingDocument.isEmbedded(); final String requestPath = containingDocument.getRequestPath(); final String xformsSubmissionPath; { if (! containingDocument.getDeploymentType().equals(XFormsConstants.DeploymentType.standalone) || containingDocument.getContainerType().equals("portlet") || isEmbeddedClient) { // Integrated or separate deployment mode or portlet xformsSubmissionPath = "/xforms-server-submit"; } else { // Plain deployment mode: submission posts to URL of the current page and xforms-xml-submission.xpl intercepts that xformsSubmissionPath = requestPath; } } // Noscript panel is included before the xhtml:form element, in case the form is hidden through CSS if (!handlerContext.isNoScript()) { helper.element("", XMLNames.XIncludeURI(), "include", new String[] { "href", getIncludedResourceURL(requestPath, "noscript-panel.xml"), "fixup-xml-base", "false" }); } final StringBuilder sb = new StringBuilder("xforms-form"); sb.append(handlerContext.isNoScript() ? " xforms-noscript" : " xforms-initially-hidden"); // LHHA appearance classes // // NOTE: There can be multiple appearances at the same time: // // - `xforms-hint-appearance-full xforms-hint-appearance-minimal` // - `xforms-hint-appearance-tooltip xforms-hint-appearance-minimal` // // That's because the `minimal` appearance doesn't apply to all controls, but only (as of 2016.2) to input fields. // AppearanceTrait$.MODULE$.encodeAndAppendAppearances(sb, "label", containingDocument.getLabelAppearances()); final scala.collection.immutable.Set<String> hintAppearances = containingDocument.getHintAppearances(); AppearanceTrait$.MODULE$.encodeAndAppendAppearances(sb, "hint", hintAppearances); if (hintAppearances.contains("tooltip")) sb.append(" xforms-enable-hint-as-tooltip"); else sb.append(" xforms-disable-hint-as-tooltip"); AppearanceTrait$.MODULE$.encodeAndAppendAppearance(sb, "help", QName.get(containingDocument.getHelpAppearance())); // Create xhtml:form element // NOTE: Do multipart as well with portlet client to simplify the proxying so we don't have to re-encode parameters final boolean doMultipartPOST = containingDocument.getStaticOps().hasControlByName("upload") || isEmbeddedClient; helper.startElement(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "form", new String[] { // Add id so that things work in portals "id", XFormsUtils.getFormId(containingDocument), // Regular classes "class", sb.toString(), // Submission parameters "action", xformsSubmissionPath, "method", "POST", // In noscript mode, don't add event handler "onsubmit", handlerContext.isNoScript() ? null : "return false", doMultipartPOST ? "enctype" : null, doMultipartPOST ? "multipart/form-data" : null}); { // Output encoded static and dynamic state helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[] { "type", "hidden", "name", "$uuid", "value", containingDocument.getUUID() }); // NOTE: we don't need $sequence here as HTML form posts are either: // // - 2nd phase of replace="all" submission: we don't (and can't) retry // - background upload: we don't want a sequence number as this run in parallel // - noscript mode: we don't (and can't) retry // // NOTE: Keep empty static state and dynamic state until client is able to deal without them final String clientEncodedStaticState = XFormsStateManager.instance().getClientEncodedStaticState(containingDocument); // if (clientEncodedStaticState != null) { helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[] { "type", "hidden", "name", "$static-state", "value", clientEncodedStaticState }); // } final Option<String> clientEncodedDynamicStateOpt = XFormsStateManager.instance().getClientEncodedDynamicState(containingDocument); // if (clientEncodedDynamicState != null) { helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[] { "type", "hidden", "name", "$dynamic-state", "value", clientEncodedDynamicStateOpt.isDefined() ? clientEncodedDynamicStateOpt.get() : null }); // } } if (!handlerContext.isNoScript()) { // Other fields used by JavaScript helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[] { "type", "hidden", "name", "$server-events", "value", "" }); helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[] { "type", "text", "name", "$client-state", "value", "", "class", "xforms-initially-hidden" }); // Store information about nested repeats hierarchy helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[]{ "type", "hidden", "name", "$repeat-tree", "value", containingDocument.getStaticOps().getRepeatHierarchyString(containingDocument.getContainerNamespace()) }); // Store information about the initial index of each repeat helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[]{ "type", "hidden", "name", "$repeat-indexes", "value", XFormsRepeatControl.currentNamespacedIndexesString(containingDocument) }); // Ajax error panel XFormsError.outputAjaxErrorPanel(containingDocument, helper, htmlPrefix); // Help panel helper.element("", XMLNames.XIncludeURI(), "include", new String[] { "href", getIncludedResourceURL(requestPath, "help-panel.xml"), "fixup-xml-base", "false" }); // Templates { final String spanQName = XMLUtils.buildQName(htmlPrefix, "span"); // HACK: We would be ok with just one template, but IE 6 doesn't allow setting the input/@type attribute properly // xf:select[@appearance = 'full'], xf:input[@type = 'xs:boolean'] XFormsSelect1Handler.outputItemFullTemplate(this, xmlReceiver, htmlPrefix, spanQName, containingDocument, reusableAttributes, attributes, "xforms-select-full-template", "$xforms-item-name$", true, "checkbox"); // xf:select1[@appearance = 'full'] XFormsSelect1Handler.outputItemFullTemplate(this, xmlReceiver, htmlPrefix, spanQName, containingDocument, reusableAttributes, attributes, "xforms-select1-full-template", "$xforms-item-name$", false, "radio"); } } else { // Noscript mode helper.element(htmlPrefix, XMLConstants.XHTML_NAMESPACE_URI, "input", new String[]{ "type", "hidden", "name", "$noscript", "value", "true" }); // Noscript error panel XFormsError.outputNoscriptErrorPanel(containingDocument, helper, htmlPrefix); } } private abstract static class Matcher implements ElementHandlerController.Matcher<ElementAnalysis> { public String getPrefixedId(Attributes attributes, Object handlerContext) { final HandlerContext hc = (HandlerContext) handlerContext; return hc.getPrefixedId(attributes); } public ElementAnalysis getElementAnalysis(Attributes attributes, Object handlerContext) { final HandlerContext hc = (HandlerContext) handlerContext; final String prefixedId = hc.getPrefixedId(attributes); if (prefixedId != null) { return hc.getPartAnalysis().getControlAnalysis(prefixedId); } else { return null; } } public boolean hasAppearance(ElementAnalysis elementAnalysis, QName appearance) { return XFormsControl$.MODULE$.appearances(elementAnalysis).contains(appearance); } public final ElementAnalysis match(Attributes attributes, Object handlerContext) { return doesMatch(attributes, handlerContext) ? getElementAnalysis(attributes, handlerContext) : null; } abstract boolean doesMatch(Attributes attributes, Object handlerContext); } private static class AppearanceMatcher extends Matcher { private final QName appearance; public AppearanceMatcher(QName appearance) { this.appearance = appearance; } public boolean doesMatch(Attributes attributes, Object handlerContext) { return hasAppearance(getElementAnalysis(attributes, handlerContext), appearance); } } private static class AnyMatcher extends Matcher { boolean doesMatch(Attributes attributes, Object handlerContext) { return true; } } public static final Matcher ANY_MATCHER = new AnyMatcher(); public static void registerHandlers(final ElementHandlerController controller, final XFormsContainingDocument containingDocument) { // Add handlers for custom components final StaticStateGlobalOps ops = containingDocument.getStaticOps(); controller.registerHandler(XXFormsComponentHandler.class.getName(), new Matcher() { public boolean doesMatch(Attributes attributes, Object handlerContext) { return ops.getBinding(getPrefixedId(attributes, handlerContext)).isDefined(); } }); // xf:input controller.registerHandler(XFormsInputHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "input", ANY_MATCHER); // xf:output controller.registerHandler(XFormsOutputTextHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "output", new AppearanceMatcher(XFormsConstants.XXFORMS_TEXT_APPEARANCE_QNAME)); controller.registerHandler(XFormsOutputDownloadHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "output", new AppearanceMatcher(XFormsConstants.XXFORMS_DOWNLOAD_APPEARANCE_QNAME)); controller.registerHandler(XFormsOutputImageHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "output", new Matcher() { public boolean doesMatch(Attributes attributes, Object handlerContext) { // TODO: aks ElementAnalysis for its mediatype final String mediatypeValue = attributes.getValue("mediatype"); return mediatypeValue != null && mediatypeValue.startsWith("image/"); } }); controller.registerHandler(XFormsOutputHTMLHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "output", new Matcher() { public boolean doesMatch(Attributes attributes, Object handlerContext) { // TODO: aks ElementAnalysis for its mediatype final String mediatypeValue = attributes.getValue("mediatype"); return mediatypeValue != null && mediatypeValue.equals("text/html"); } }); controller.registerHandler(XFormsOutputDefaultHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "output", ANY_MATCHER); // xf:trigger final Matcher triggerSubmitMinimalMatcher = new AppearanceMatcher(XFormsConstants.XFORMS_MINIMAL_APPEARANCE_QNAME) { public boolean doesMatch(Attributes attributes, Object handlerContext) { // in noscript mode, use the full appearance return ! containingDocument.noscript() && super.doesMatch(attributes, handlerContext); } }; controller.registerHandler(XFormsTriggerMinimalHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "trigger", triggerSubmitMinimalMatcher); controller.registerHandler(XFormsTriggerFullHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "trigger", ANY_MATCHER); // xf:submit controller.registerHandler(XFormsTriggerMinimalHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "submit", triggerSubmitMinimalMatcher); controller.registerHandler(XFormsTriggerFullHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "submit", ANY_MATCHER); // xf:group controller.registerHandler(XFormsGroupInternalHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "group", new AppearanceMatcher(XFormsConstants.XXFORMS_INTERNAL_APPEARANCE_QNAME)); controller.registerHandler(XFormsGroupSeparatorHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "group", new Matcher() { public boolean doesMatch(Attributes attributes, Object handlerContext) { // XFormsAnnotator adds this appearance if needed // See: https://github.com/orbeon/orbeon-forms/issues/418 final String appearanceAttributeValue = attributes.getValue(XFormsConstants.APPEARANCE_QNAME.getName()); return XFormsConstants.XXFORMS_SEPARATOR_APPEARANCE_QNAME.getQualifiedName().equals(appearanceAttributeValue); } }); controller.registerHandler(XFormsGroupFieldsetHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "group", new AppearanceMatcher(XFormsConstants.XXFORMS_FIELDSET_APPEARANCE_QNAME) { public boolean doesMatch(Attributes attributes, Object handlerContext) { return super.doesMatch(attributes, handlerContext) || LHHASupport.hasLabel(containingDocument, getPrefixedId(attributes, handlerContext)); } }); controller.registerHandler(XFormsGroupDefaultHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "group", ANY_MATCHER); // xf:switch // NOTE: We use the same handlers for switch as we do for group controller.registerHandler(XFormsGroupSeparatorHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "switch", new Matcher() { public boolean doesMatch(Attributes attributes, Object handlerContext) { // XFormsAnnotator adds this appearance if needed // See: https://github.com/orbeon/orbeon-forms/issues/418 final String appearanceAttributeValue = attributes.getValue(XFormsConstants.APPEARANCE_QNAME.getName()); return XFormsConstants.XXFORMS_SEPARATOR_APPEARANCE_QNAME.getQualifiedName().equals(appearanceAttributeValue); } }); controller.registerHandler(XFormsGroupDefaultHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "switch", ANY_MATCHER); controller.registerHandler(XFormsCaseHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "case", ANY_MATCHER); // xf:repeat controller.registerHandler(XFormsRepeatHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "repeat", ANY_MATCHER); controller.registerHandler(NullElementHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "repeat-iteration", ANY_MATCHER); // xf:secret controller.registerHandler(XFormsSecretHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "secret", ANY_MATCHER); // xf:upload controller.registerHandler(XFormsUploadHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "upload", ANY_MATCHER); // xf:range controller.registerHandler(XFormsRangeHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "range", ANY_MATCHER); // Other controls controller.registerHandler(XFormsTextareaHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "textarea", ANY_MATCHER); if (!containingDocument.noscript()) controller.registerHandler(XXFormsDialogHandler.class.getName(), XFormsConstants.XXFORMS_NAMESPACE_URI, "dialog", ANY_MATCHER); else controller.registerHandler(NullHandler.class.getName(), XFormsConstants.XXFORMS_NAMESPACE_URI, "dialog", ANY_MATCHER); // xf:select and xf:select1 controller.registerHandler(XFormsSelect1InternalHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "select", new AppearanceMatcher(XFormsConstants.XXFORMS_INTERNAL_APPEARANCE_QNAME)); controller.registerHandler(XFormsSelect1InternalHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "select1", new AppearanceMatcher(XFormsConstants.XXFORMS_INTERNAL_APPEARANCE_QNAME)); controller.registerHandler(XFormsSelectHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "select", ANY_MATCHER); controller.registerHandler(XFormsSelect1Handler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "select1", ANY_MATCHER); // Add handlers for LHHA elements controller.registerHandler(XFormsLHHAHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "label", ANY_MATCHER); controller.registerHandler(XFormsLHHAHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "help", ANY_MATCHER); controller.registerHandler(XFormsLHHAHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "hint", ANY_MATCHER); controller.registerHandler(XFormsLHHAHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI, "alert", ANY_MATCHER); // xxf:dynamic controller.registerHandler(XXFormsDynamicHandler.class.getName(), XFormsConstants.XXFORMS_NAMESPACE_URI, "dynamic", ANY_MATCHER); } public void end(String uri, String localname, String qName) throws SAXException { // Add global top-level XBL markup final scala.collection.Iterator<XBLBindings.Global> i = containingDocument.getStaticOps().getGlobals().iterator(); while (i.hasNext()) XXFormsComponentHandler.processShadowTree(handlerContext.getController(), i.next().templateTree()); // Close xhtml:form helper.endElement(); // Close xhtml:body final ContentHandler contentHandler = handlerContext.getController().getOutput(); contentHandler.endElement(uri, localname, qName); } public static String getIncludedResourcePath(String requestPath, String fileName) { // Path will look like "/app-name/whatever" final String[] pathElements = StringUtils.split(requestPath, '/'); if (pathElements.length >= 2) { final String appName = pathElements[0];// it seems that split() does not return first blank match final String path = "/apps/" + appName + "/" + fileName; if (ResourceManagerWrapper.instance().exists(path)) { return path; } } // Default return "/config/" + fileName; } public static String getIncludedResourceURL(String requestPath, String fileName) { return "oxf:" + getIncludedResourcePath(requestPath, fileName); } }