/**
* 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.common.ValidationException;
import org.orbeon.oxf.xforms.XFormsConstants;
import org.orbeon.oxf.xforms.XFormsUtils;
import org.orbeon.oxf.xforms.analysis.controls.AppearanceTrait;
import org.orbeon.oxf.xforms.analysis.controls.ComponentControl;
import org.orbeon.oxf.xforms.analysis.controls.LHHAAnalysis;
import org.orbeon.oxf.xforms.analysis.model.ValidationLevel;
import org.orbeon.oxf.xforms.control.XFormsControl;
import org.orbeon.oxf.xforms.control.XFormsControlFactory;
import org.orbeon.oxf.xforms.control.XFormsSingleNodeControl;
import org.orbeon.oxf.xforms.control.XFormsValueControl;
import org.orbeon.oxf.xforms.processor.handlers.HandlerContext;
import org.orbeon.oxf.xforms.processor.handlers.XFormsBaseHandler;
import org.orbeon.oxf.xml.*;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* Base class for all XHTML and XForms element handlers.
*/
public abstract class XFormsBaseHandlerXHTML extends XFormsBaseHandler {
protected XFormsBaseHandlerXHTML(boolean repeating, boolean forwarding) {
super(repeating, forwarding);
}
protected HandlerContext getHandlerContext() {
return this.handlerContext;
}
private void addConstraintClasses(StringBuilder sb, scala.Option<ValidationLevel> constraintLevel) {
if (constraintLevel.isDefined()) {
final String levelName = constraintLevel.get().entryName();
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-");
sb.append(levelName.equals("error") ? "invalid" : levelName);// level is called "error" but we use "invalid" on the client
}
}
final public void handleMIPClasses(StringBuilder sb, String controlPrefixedId, XFormsControl control) {
// Output MIP classes
// TODO: Move this to to control itself, like writeMIPsAsAttributes
// xforms-disabled class
// NOTE: We used to not output this class if the control existed and didn't have a binding. That looked either
// like an inconsistent optimization (not done for controls with bindings), or like an oversight (likely).
final boolean isRelevant = control != null && control.isRelevant();
if (! isRelevant && ! handlerContext.isTemplate()) { // don't output class within a template
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-disabled");
}
// MIP classes for a concrete control
if (isRelevant && containingDocument.getStaticOps().hasBinding(controlPrefixedId)) {
// Output standard MIP classes
if (control.visited()) {
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-visited");
}
if (control instanceof XFormsSingleNodeControl) {
// TODO: inherit from this method instead rather than using instanceof
final XFormsSingleNodeControl singleNodeControl = (XFormsSingleNodeControl) control;
addConstraintClasses(sb, singleNodeControl.alertLevel());
if (singleNodeControl.isReadonly()) {
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-readonly");
}
if (singleNodeControl.isRequired()) {
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-required");
if (control instanceof XFormsValueControl) {
// NOTE: Test above excludes xf:group
if (((XFormsValueControl) control).isEmptyValue())
sb.append(" xforms-empty");
else
sb.append(" xforms-filled");
}
}
// Output custom MIPs classes
final String customMIPs = singleNodeControl.jCustomMIPsClassesAsString();
if (! customMIPs.equals("")) {
if (sb.length() > 0)
sb.append(' ');
sb.append(customMIPs);
}
// Output type class
final String typeCSSClass = singleNodeControl.getBuiltinOrCustomTypeCSSClassOrNull();
if (typeCSSClass != null) {
if (sb.length() > 0)
sb.append(' ');
sb.append(typeCSSClass);
}
}
}
}
final protected StringBuilder getInitialClasses(String controlURI, String controlName, Attributes controlAttributes, XFormsControl control, boolean incrementalDefault) {
final StringBuilder sb = new StringBuilder(50);
// User-defined classes go first
appendControlUserClasses(controlAttributes, control, sb);
// Component control doesn't get xforms-control, xforms-[control name], incremental, mediatype, xforms-static
if (! (elementAnalysis instanceof ComponentControl)) {
{
if (sb.length() > 0)
sb.append(' ');
// We only call xforms-control the actual controls as per the spec
// NOTE: XForms 1.1 has core and container controls but our client depends on having `xforms-control`.
if (! XFormsControlFactory.isContainerControl(controlURI, controlName))
sb.append("xforms-control xforms-");
else
sb.append("xforms-");
sb.append(controlName);
}
{
// Class for incremental mode
final String value = controlAttributes.getValue("incremental");
// Set the class if the default is non-incremental and the user explicitly set the value to true, or the
// default is incremental and the user did not explicitly set it to false
if ((!incrementalDefault && "true".equals(value)) || (incrementalDefault && !"false".equals(value))) {
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-incremental");
}
}
{
// Class for mediatype
final String mediatypeValue = controlAttributes.getValue("mediatype");
if (mediatypeValue != null) {
// NOTE: We could certainly do a better check than this to make sure we have a valid mediatype
final int slashIndex = mediatypeValue.indexOf('/');
if (slashIndex == -1)
throw new ValidationException("Invalid mediatype attribute value: " + mediatypeValue, handlerContext.getLocationData());
if (sb.length() > 0)
sb.append(' ');
sb.append("xforms-mediatype-");
if (mediatypeValue.endsWith("/*")) {
// Add class with just type: "image/*" -> "xforms-mediatype-image"
sb.append(mediatypeValue.substring(0, mediatypeValue.length() - 2));
} else {
// Add class with type and subtype: "text/html" -> "xforms-mediatype-text-html"
sb.append(mediatypeValue.replace('/', '-'));
// Also add class with just type: "image/jpeg" -> "xforms-mediatype-image"
sb.append(" xforms-mediatype-");
sb.append(mediatypeValue.substring(0, slashIndex));
}
}
}
// Static read-only
if (isStaticReadonly(control))
sb.append(" xforms-static");
}
// Classes for appearances
if (elementAnalysis instanceof AppearanceTrait)
((AppearanceTrait) elementAnalysis).encodeAndAppendAppearances(sb);
return sb;
}
final protected StringBuilder appendControlUserClasses(Attributes controlAttributes, XFormsControl control, StringBuilder sb) {
// @class
final String attributeValue = controlAttributes.getValue("class");
final String value;
if (attributeValue != null) {
if (! XFormsUtils.maybeAVT(attributeValue)) {
// Definitely not an AVT
value = attributeValue;
} else {
// Possible AVT
if (control != null) {
// Ask the control if possible
value = control.jExtensionAttributeValue(XFormsConstants.CLASS_QNAME);
} else {
// Otherwise we can't compute it
value = null;
}
}
if (value != null) {
if (sb.length() > 0)
sb.append(' ');
sb.append(value);
}
}
return sb;
}
final protected void handleLabelHintHelpAlert(
LHHAAnalysis lhhaAnalysis,
String targetControlEffectiveId,
String forEffectiveId,
XFormsBaseHandler.LHHAC lhhaType,
String requestedElementName,
XFormsControl control,
boolean isTemplate,
boolean isExternal
) throws SAXException {
final AttributesImpl staticLHHAAttributes = Dom4jUtils.getSAXAttributes(lhhaAnalysis.element());
final boolean isLabel = lhhaType == LHHAC.LABEL;
final boolean isHelp = lhhaType == LHHAC.HELP;
final boolean isHint = lhhaType == LHHAC.HINT;
final boolean isAlert = lhhaType == LHHAC.ALERT;
if (staticLHHAAttributes != null || isAlert) {
// If no attributes were found, there is no such label / help / hint / alert
if (handlerContext.isNoScript() && isHelp) {
if (control != null) {
final ContentHandler contentHandler = handlerContext.getController().getOutput();
final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
// <a href="#my-control-id-help">
final AttributesImpl aAttributes = new AttributesImpl();
aAttributes.addAttribute("", "href", "href", XMLReceiverHelper.CDATA, "#" + getLHHACId(containingDocument, targetControlEffectiveId, LHHAC_CODES.get(LHHAC.HELP)));
aAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-help-anchor");
final String aQName = XMLUtils.buildQName(xhtmlPrefix, "a");
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, "a", aQName, aAttributes);
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "a", aQName);
}
} else {
final String labelHintHelpAlertValue;
final boolean mustOutputHTMLFragment;
if (control != null) {
// Get actual value from control
if (isLabel) {
labelHintHelpAlertValue = control.getLabel();
mustOutputHTMLFragment = control.isHTMLLabel();
} else if (isHelp) {
// NOTE: Special case here where we get the escaped help to facilitate work below. Help is a special
// case because it is stored as escaped HTML within a <label> element.
labelHintHelpAlertValue = control.getEscapedHelp();
mustOutputHTMLFragment = false;
} else if (isHint) {
labelHintHelpAlertValue = control.getHint();
mustOutputHTMLFragment = control.isHTMLHint();
} else if (isAlert) {
labelHintHelpAlertValue = control.getAlert();
mustOutputHTMLFragment = control.isHTMLAlert();
} else {
throw new IllegalStateException("Illegal type requested");
}
} else {
// Placeholder
labelHintHelpAlertValue = null;
mustOutputHTMLFragment = false;
}
final String elementName;
{
if (requestedElementName != null) {
elementName = requestedElementName;
} else if (isLabel) {
elementName = handlerContext.getLabelElementName();
} else if (isHelp) {
elementName = handlerContext.getHelpElementName();
} else if (isHint) {
elementName = handlerContext.getHintElementName();
} else if (isAlert) {
elementName = handlerContext.getAlertElementName();
} else {
throw new IllegalStateException("Illegal type requested");
}
}
final StringBuilder classes = new StringBuilder(30);
// Put user classes first if any
if (staticLHHAAttributes != null) {
final String userClass = staticLHHAAttributes.getValue("class");
if (userClass != null)
classes.append(userClass);
}
// Mark alert as active if needed
if (isAlert) {
if (control instanceof XFormsSingleNodeControl) {
final XFormsSingleNodeControl singleNodeControl = (XFormsSingleNodeControl) control;
final scala.Option<ValidationLevel> constraintLevel = singleNodeControl.alertLevel();
if (constraintLevel.isDefined()) {
if (classes.length() > 0)
classes.append(' ');
classes.append("xforms-active");
}
// Constraint classes are placed on the control if the alert is not external
if (isExternal)
addConstraintClasses(classes, constraintLevel);
}
}
// Handle visibility
// TODO: It would be great to actually know about the relevance of help, hint, and label. Right now, we just look at whether the value is empty
if (control != null) {
if (!control.isRelevant()) {
if (classes.length() > 0)
classes.append(' ');
classes.append("xforms-disabled");
}
} else if (!isTemplate || isHelp) {
// Null control outside of template OR help within template
if (classes.length() > 0)
classes.append(' ');
classes.append("xforms-disabled");
}
// LHHA name
if (classes.length() > 0)
classes.append(' ');
classes.append("xforms-");
classes.append(lhhaType.name().toLowerCase());
// We handle null attributes as well because we want a placeholder for "alert" even if there is no xf:alert
final Attributes newAttributes = (staticLHHAAttributes != null) ? staticLHHAAttributes : new AttributesImpl();
lhhaAnalysis.encodeAndAppendAppearances(classes);
outputLabelFor(
handlerContext,
getIdClassXHTMLAttributes(newAttributes, classes.toString(), null),
targetControlEffectiveId,
forEffectiveId,
lhhaType,
elementName,
labelHintHelpAlertValue,
mustOutputHTMLFragment,
isExternal
);
}
}
}
final protected static void outputLabelFor(
HandlerContext handlerContext,
Attributes attributes,
String targetControlEffectiveId,
String forEffectiveId,
XFormsBaseHandler.LHHAC lhha,
String elementName,
String labelValue,
boolean mustOutputHTMLFragment,
boolean addIds
) throws SAXException {
outputLabelForStart(handlerContext, attributes, targetControlEffectiveId, forEffectiveId, lhha, elementName, addIds);
outputLabelText(handlerContext.getController().getOutput(), null, labelValue, handlerContext.findXHTMLPrefix(), mustOutputHTMLFragment);
outputLabelForEnd(handlerContext, elementName);
}
final protected static void outputLabelForStart(
HandlerContext handlerContext,
Attributes attributes,
String targetControlEffectiveId,
String forEffectiveId,
XFormsBaseHandler.LHHAC lhha,
String elementName,
boolean addIds
) throws SAXException {
assert lhha != null;
assert ! addIds || targetControlEffectiveId != null;
// Replace id attribute to be foo-label, foo-hint, foo-help, or foo-alert
final AttributesImpl newAttribute;
if (addIds && targetControlEffectiveId != null) {
// Add or replace existing id attribute
// NOTE: addIds == true for external LHHA
newAttribute = SAXUtils.addOrReplaceAttribute(attributes, "", "", "id", getLHHACId(handlerContext.getContainingDocument(), targetControlEffectiveId, LHHAC_CODES.get(lhha)));
} else {
// Remove existing id attribute if any
newAttribute = SAXUtils.removeAttribute(attributes, "", "id");
}
// Add @for attribute if specified and element is a label
if (forEffectiveId != null && elementName.equals("label"))
newAttribute.addAttribute("", "for", "for", XMLReceiverHelper.CDATA, forEffectiveId);
final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
final String labelQName = XMLUtils.buildQName(xhtmlPrefix, elementName);
final ContentHandler contentHandler = handlerContext.getController().getOutput();
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, elementName, labelQName, newAttribute);
}
final protected static void outputLabelForEnd(HandlerContext handlerContext, String elementName) throws SAXException {
final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
final String labelQName = XMLUtils.buildQName(xhtmlPrefix, elementName);
final XMLReceiver xmlReceiver = handlerContext.getController().getOutput();
xmlReceiver.endElement(XMLConstants.XHTML_NAMESPACE_URI, elementName, labelQName);
}
public static void outputLabelText(XMLReceiver xmlReceiver, XFormsControl xformsControl, String value, String xhtmlPrefix, boolean html) throws SAXException {
if (StringUtils.isNotEmpty(value)) {
if (html)
XFormsUtils.streamHTMLFragment(xmlReceiver, value, xformsControl != null ? xformsControl.getLocationData() : null, xhtmlPrefix);
else
xmlReceiver.characters(value.toCharArray(), 0, value.length());
}
}
public static void outputDisabledAttribute(AttributesImpl newAttributes) {
// @disabled="disabled"
// HTML 4: @disabled supported on: input, button, select, optgroup, option, and textarea.
newAttributes.addAttribute("", "disabled", "disabled", XMLReceiverHelper.CDATA, "disabled");
}
}