/** * 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.oxf.xforms.StaticStateGlobalOps; import org.orbeon.oxf.xforms.XFormsConstants; import org.orbeon.oxf.xforms.analysis.ElementAnalysis; import org.orbeon.oxf.xforms.analysis.controls.StaticLHHASupport; import org.orbeon.oxf.xforms.control.XFormsControl; import org.orbeon.oxf.xforms.processor.handlers.XFormsControlLifecycleHandlerDelegate; import org.orbeon.oxf.xml.XMLReceiverHelper; import org.orbeon.oxf.xml.XMLConstants; import org.orbeon.oxf.xml.XMLUtils; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; /** * This class is a helper base class which handles the lifecycle of producing markup for a control. The following * phases are handled: * * - Give the handler a chance to do some prep work: prepareHandler() * - Get custom information: addCustomClasses() * - Check whether the control wants any output at all: isMustOutputControl() * - Output label, control, hint, help, and alert in order specified by properties * * Outputting the control is split into two parts: handleControlStart() and handleControlEnd(). In most cases, only * handleControlStart() is used, but container controls will use handleControlEnd(). */ public abstract class XFormsControlLifecyleHandler extends XFormsBaseHandlerXHTML { private XFormsControlLifecycleHandlerDelegate xFormsControlLifecycleHandlerDelegate; private Attributes attributes; private String[] endConfig; private String containingElementQName; protected XFormsControlLifecyleHandler(boolean repeating) { super(repeating, false); } protected XFormsControlLifecyleHandler(boolean repeating, boolean forwarding) { super(repeating, forwarding); } protected String getContainingElementName() { // By default, controls are enclosed with a <span> return "span"; } protected String getContainingElementQName() { if (containingElementQName == null) { final String xhtmlPrefix = handlerContext.findXHTMLPrefix(); containingElementQName = XMLUtils.buildQName(xhtmlPrefix, getContainingElementName()); } return containingElementQName; } protected boolean isTemplate() { return xFormsControlLifecycleHandlerDelegate.isTemplate(); } protected String getPrefixedId() { return xFormsControlLifecycleHandlerDelegate.prefixedId(); } protected String getEffectiveId() { return xFormsControlLifecycleHandlerDelegate.effectiveId(); } protected XFormsControl currentControlOrNull() { return xFormsControlLifecycleHandlerDelegate.currentControlOrNull(); } protected scala.Option<XFormsControl> currentControlOpt() { return xFormsControlLifecycleHandlerDelegate.currentControlOpt(); } @Override public void init(String uri, String localname, String qName, Attributes attributes, Object matched) throws SAXException { super.init(uri, localname, qName, attributes, matched); xFormsControlLifecycleHandlerDelegate = new XFormsControlLifecycleHandlerDelegate(handlerContext, containingDocument, attributes); } @Override public final void start(String uri, String localname, String qName, Attributes attributes) throws SAXException { if (isMustOutputControl(currentControlOrNull())) { final ContentHandler contentHandler = handlerContext.getController().getOutput(); if (isMustOutputContainerElement()) { // Open control element, usually <span> final AttributesImpl containerAttributes = getContainerAttributes(uri, localname, attributes); contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, getContainingElementName(), getContainingElementQName(), containerAttributes); } // Get local order for control final String localOrder = attributes.getValue(XFormsConstants.XXFORMS_ORDER_QNAME.getNamespaceURI(), XFormsConstants.XXFORMS_ORDER_QNAME.getName()); // Use local or default config final String[] config = (localOrder != null) ? StringUtils.split(localOrder) : handlerContext.getDocumentOrder(); // 2012-12-17: Removed nested <a name="effective-id"> because the enclosing <span> for the control has the // same id and will be handled first by the browser as per HTML 5. This means the named anchor is actually // redundant. // Process everything up to and including the control for (int i = 0; i < config.length; i++) { final String current = config[i]; if ("control".equals(current)) { // Handle control handleControlStart(uri, localname, qName, attributes, getEffectiveId(), currentControlOrNull()); // Do the rest in end() below if needed if (i < config.length - 1) { // There remains stuff to process final int endConfigLength = config.length - i; endConfig = new String[endConfigLength]; System.arraycopy(config, i, endConfig, 0, endConfigLength); this.attributes = new AttributesImpl(attributes); } break; } else if ("label".equals(current)) { // xf:label if (hasLocalLabel()) handleLabel(); } else if ("alert".equals(current)) { // xf:alert if (hasLocalAlert()) handleAlert(); } else if ("hint".equals(current)) { // xf:hint if (hasLocalHint()) handleHint(); } else { // xf:help if (hasLocalHelp()) handleHelp(); } } } } @Override public final void end(String uri, String localname, String qName) throws SAXException { if (isMustOutputControl(currentControlOrNull())) { // Process everything after the control has been shown if (endConfig != null) { for (final String current : endConfig) { if ("control".equals(current)) { // Handle control handleControlEnd(uri, localname, qName, attributes, getEffectiveId(), currentControlOrNull()); } else if ("label".equals(current)) { // xf:label if (hasLocalLabel()) handleLabel(); } else if ("alert".equals(current)) { // xf:alert if (hasLocalAlert()) handleAlert(); } else if ("hint".equals(current)) { // xf:hint if (hasLocalHint()) handleHint(); } else { // xf:help if (hasLocalHelp()) handleHelp(); } } } if (isMustOutputContainerElement()) { // Close control element, usually <span> final ContentHandler contentHandler = handlerContext.getController().getOutput(); contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, getContainingElementName(), getContainingElementQName()); } } } private boolean hasLocalLabel() { return hasLocalLHHA("label"); } private boolean hasLocalHint() { return hasLocalLHHA("hint"); } private boolean hasLocalHelp() { return hasLocalLHHA("help"); } private boolean hasLocalAlert() { return hasLocalLHHA("alert"); } private boolean hasLocalLHHA(String lhhaType) { final StaticStateGlobalOps globalOps = containingDocument.getStaticOps(); final ElementAnalysis analysis = globalOps.getControlAnalysis(getPrefixedId()); if (analysis instanceof StaticLHHASupport) return ((StaticLHHASupport) analysis).hasLocal(lhhaType); else return false; } protected boolean isMustOutputControl(XFormsControl control) { // May be overridden by subclasses return true; } protected boolean isMustOutputContainerElement() { // May be overridden by subclasses return true; } protected void addCustomClasses(StringBuilder classes, XFormsControl control) { // May be overridden by subclasses } protected boolean isDefaultIncremental() { // May be overridden by subclasses return false; } protected void handleLabel() throws SAXException { handleLabelHintHelpAlert( getStaticLHHA(getPrefixedId(), LHHAC.LABEL), getEffectiveId(), getForEffectiveId(getEffectiveId()), LHHAC.LABEL, isStaticReadonly(currentControlOrNull()) ? "span" : null, currentControlOrNull(), isTemplate(), false ); } protected void handleAlert() throws SAXException { if (! isStaticReadonly(currentControlOrNull())) handleLabelHintHelpAlert( getStaticLHHA(getPrefixedId(), LHHAC.ALERT), getEffectiveId(), getForEffectiveId(getEffectiveId()), LHHAC.ALERT, null, currentControlOrNull(), isTemplate(), false ); } protected void handleHint() throws SAXException { if (! isStaticReadonly(currentControlOrNull())) handleLabelHintHelpAlert( getStaticLHHA(getPrefixedId(), LHHAC.HINT), getEffectiveId(), getForEffectiveId(getEffectiveId()), LHHAC.HINT, null, currentControlOrNull(), isTemplate(), false ); } protected void handleHelp() throws SAXException { if (! isStaticReadonly(currentControlOrNull())) handleLabelHintHelpAlert( getStaticLHHA(getPrefixedId(), LHHAC.HELP), getEffectiveId(), getForEffectiveId(getEffectiveId()), LHHAC.HELP, null, currentControlOrNull(), isTemplate(), false ); } // Must be overridden by subclasses protected abstract void handleControlStart(String uri, String localname, String qName, Attributes attributes, String effectiveId, XFormsControl control) throws SAXException; protected void handleControlEnd(String uri, String localname, String qName, Attributes attributes, String effectiveId, XFormsControl control) throws SAXException { // May be overridden by subclasses } protected AttributesImpl getEmptyNestedControlAttributesMaybeWithId(String uri, String localname, Attributes attributes, String effectiveId, XFormsControl control, boolean addId) { reusableAttributes.clear(); final AttributesImpl containerAttributes = reusableAttributes; if (addId) containerAttributes.addAttribute("", "id", "id", XMLReceiverHelper.CDATA, getLHHACId(containingDocument, effectiveId, LHHAC_CODES.get(LHHAC.CONTROL))); return containerAttributes; } private AttributesImpl getContainerAttributes(String uri, String localname, Attributes attributes) { // NOTE: Only reason we do not use the class members directly is to handle boolean xf:input, which delegates // its output to xf:select1. Should be improved some day. final String prefixedId = getPrefixedId(); final String effectiveId = getEffectiveId(); final XFormsControl xformsControl = currentControlOrNull(); // Get classes final StringBuilder classes; { // Initial classes: xforms-control, xforms-[control name], incremental, appearance, mediatype, xforms-static classes = getInitialClasses(uri, localname, attributes, xformsControl, isDefaultIncremental()); // All MIP-related classes handleMIPClasses(classes, prefixedId, xformsControl); // Static classes containingDocument.getStaticOps().appendClasses(classes, prefixedId); // Dynamic classes added by the control addCustomClasses(classes, xformsControl); } // Get attributes final AttributesImpl newAttributes = getIdClassXHTMLAttributes(attributes, classes.toString(), effectiveId); // Add extension attributes in no namespace if possible if (xformsControl != null) { xformsControl.addExtensionAttributesExceptClassAndAcceptForHandler(newAttributes, ""); } return newAttributes; } /** * Return the effective id of the element to which label/@for, etc. must point to. */ public String getForEffectiveId(String effectiveId) { // Default: point to foo$bar$$c.1-2-3 return getLHHACId(containingDocument, getEffectiveId(), LHHAC_CODES.get(LHHAC.CONTROL)); } }