/**
* 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.analysis;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.properties.PropertySet;
import org.orbeon.oxf.xforms.XFormsConstants;
import org.orbeon.oxf.xforms.XFormsProperties;
import org.orbeon.oxf.xforms.XFormsUtils;
import org.orbeon.oxf.xforms.xbl.IndexableBinding;
import org.orbeon.oxf.xml.*;
import org.orbeon.oxf.xml.dom4j.ExtendedLocationData;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* XMLReceiver that:
*
* - adds ids on all the XForms elements which don't have one
* - gathers namespace information on XForms elements (xf:*, xxf:*, exf:*, xbl:*).
* - finds AVTs on non-XForms elements
* - adds ids to those elements
* - produces xxf:attribute elements
* - finds title information and produces xxf:text elements
* - register xbl:binding mappings
* - add ids to elements with XBL bindings
* - insert fr:xforms-inspector if requested and needed
*
* NOTE: There was a thought of merging this with XFormsExtractor but we need a separate annotated document in
* XFormsToXHTML to produce the output. So if we modify this, we should modify it so that two separate XMLReceiver (at
* least two separate outputs) are produced, one for the annotated output, another for the extracted output.
*/
public class XFormsAnnotator extends XFormsAnnotatorBase implements XMLReceiver {
private SAXStore templateSAXStore;
private int level = 0;
private boolean inHead; // whether we are in the HTML head
private boolean inBody; // whether we are in the HTML body
private boolean inTitle; // whether we are in the HTML title
private boolean inXForms; // whether we are in a model or other XForms content
private int xformsLevel;
private boolean inPreserve; // whether we are in LHHA, schema or instance
private int preserveLevel;
private boolean inLHHA; // whether we are in LHHA (meaningful only if inPreserve == true)
private boolean inXBL; // whether we are in xbl:xbl (meaningful only if inPreserve == true)
private boolean inXBLBinding; // whether we are in the body of an XBL binding (meaningful only if inPreserve == true)
private final boolean isTopLevel;
private final boolean isRestore;
private final Metadata metadata;
private final boolean isGenerateIds;
private NamespaceContext namespaceContext = new NamespaceContext();
private final boolean hostLanguageAVTs = XFormsProperties.isHostLanguageAVTs(); // TODO: this should be obtained per document, but we only know about this in the extractor
private final AttributesImpl reusableAttributes = new AttributesImpl();
private final String[] reusableStringArray = new String[1];
private String htmlTitleElementId;
@Override
public boolean isInXBLBinding() {
return inXBLBinding;
}
@Override
public boolean isInPreserve() {
return inPreserve;
}
/**
* Constructor for XBL shadow trees and top-level documents.
*
* @param templateReceiver template output (special treatment for marks if this is a SAXStore)
* @param extractorReceiver extractor output (can be null for XBL for now)
* @param metadata metadata to gather
*/
public XFormsAnnotator(XMLReceiver templateReceiver, XMLReceiver extractorReceiver, Metadata metadata, boolean isTopLevel) {
super(templateReceiver, extractorReceiver);
this.metadata = metadata;
this.isGenerateIds = true;
this.isTopLevel = isTopLevel;
this.isRestore = false;
if (templateReceiver instanceof SAXStore)
this.templateSAXStore = (SAXStore) templateReceiver;
}
/**
* This constructor just computes the namespace mappings and AVT elements and gathers id information.
*/
public XFormsAnnotator(Metadata metadata) {
super(null, null);
// In this mode, all elements that need to have ids already have them, so set safe defaults
this.metadata = metadata;
this.isGenerateIds = false;
this.isTopLevel = true;
this.isRestore = true;
metadata.initializeBindingLibraryIfNeeded();
}
@Override
public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException {
namespaceContext.startElement();
final StackElement stackElement = startElement(uri, localname);
final int idIndex = attributes.getIndex("id");
// Entering model or controls
if (!inXForms && stackElement.isXFormsOrBuiltinExtension()) {
inXForms = true;
xformsLevel = level;
}
if (inPreserve) {
// Within preserved content
if (inLHHA) {
// Gather id and namespace information about content of LHHA
if (stackElement.isXForms()) {
// Must be xf:output
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
} else if (hostLanguageAVTs && hasAVT(attributes)) {
// Must be an AVT on an host language element
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
}
} else if (inXBL && level - 1 == preserveLevel && stackElement.isXBL() && "binding".equals(localname)) {
// Gather id and namespace information
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
// Gather binding information from xbl:xbl/xbl:binding/@element
final String elementAtt = attributes.getValue("element");
if (elementAtt != null)
metadata.registerInlineBinding(namespaceContext.current().mappings(), elementAtt, rewriteId(reusableStringArray[0]));
}
// Output element
stackElement.startElement(uri, localname, qName, attributes);
} else if (stackElement.isXBL()) {
// This must be xbl:xbl (otherwise we will have isPreserve == true) or xbl:template
assert localname.equals("xbl") || localname.equals("template") || localname.equals("handler");
// NOTE: Still process attributes, because the annotator is used to process top-level <xbl:handler> as well.
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
stackElement.startElement(uri, localname, qName, attributes);
} else {
// Only search for XBL bindings when under xh:body or if we are not at the top-level (which means that we
// are within an XBL component already). But when restoring, always search, because we don't have a distinction
// between model and view under <static-state>.
final scala.Option<IndexableBinding> bindingOpt =
(inBody || isRestore || ! isTopLevel) ? metadata.findBindingForElement(uri, localname, attributes) : NONE_INDEXABLE_BINDING;
if (bindingOpt.isDefined()) {
// Element with a binding
// Create a new id and update the attributes if needed
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
final String xformsElementId = reusableStringArray[0];
// Index binding by prefixed id
metadata.mapBindingToElement(rewriteId(xformsElementId), bindingOpt.get());
if (templateSAXStore != null) {
// Remember mark if xxf:update="full"
final String xxformsUpdate = attributes.getValue(XFormsConstants.XXFORMS_UPDATE_QNAME.getNamespaceURI(), XFormsConstants.XXFORMS_UPDATE_QNAME.getName());
if (XFormsConstants.XFORMS_FULL_UPDATE.equals(xxformsUpdate)) {
// Remember this subtree has a full update
putMark(xformsElementId);
// Add a class to help the client
attributes = SAXUtils.appendToClassAttribute(attributes, "xforms-update-full");
}
}
// Leave element untouched (except for the id attribute)
stackElement.startElement(uri, localname, qName, attributes);
// Don't handle the content
inPreserve = true;
inXBLBinding = true;
preserveLevel = level;
} else if (stackElement.isXFormsOrBuiltinExtension()) {
// This is an XForms element
// TODO: can we restrain gathering ids / namespaces to only certain elements (all controls + elements with XPath expressions + models + instances)?
// Create a new id and update the attributes if needed
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
final String xformsElementId = reusableStringArray[0];
// Handle full update annotation
if (templateSAXStore != null) {
// Remember mark if xxf:update="full"
final String xxformsUpdate = attributes.getValue(XFormsConstants.XXFORMS_UPDATE_QNAME.getNamespaceURI(), XFormsConstants.XXFORMS_UPDATE_QNAME.getName());
if (XFormsConstants.XFORMS_FULL_UPDATE.equals(xxformsUpdate)) {
// Remember this subtree has a full update
putMark(xformsElementId);
// Add a class to help the client
attributes = SAXUtils.appendToClassAttribute(attributes, "xforms-update-full");
}
}
// Rewrite elements / add appearances
if (inTitle && "output".equals(localname)) {
// Special case of xf:output within title, which produces an xxf:text control
attributes = SAXUtils.addOrReplaceAttribute(attributes, "", "", "for", htmlTitleElementId);
startPrefixMapping2("xxf", XFormsConstants.XXFORMS_NAMESPACE_URI);
stackElement.startElement(XFormsConstants.XXFORMS_NAMESPACE_URI, "text", "xxf:text", attributes);
} else if (("group".equals(localname) || "switch".equals(localname)) && doesClosestXHTMLRequireSeparatorAppearance()) {
// Closest xhtml:* ancestor is xhtml:table|xhtml:tbody|xhtml:thead|xhtml:tfoot|xhtml:tr
// Append the new xxf:separator appearance
final String existingAppearance = attributes.getValue("appearance");
// See: https://github.com/orbeon/orbeon-forms/issues/418
attributes = SAXUtils.addOrReplaceAttribute(attributes, "", "", XFormsConstants.APPEARANCE_QNAME.getName(),
(existingAppearance != null ? existingAppearance + " " : "") + XFormsConstants.XXFORMS_SEPARATOR_APPEARANCE_QNAME.getQualifiedName());
stackElement.startElement(uri, localname, qName, attributes);
} else if (stackElement.isXForms() && "repeat".equals(localname)) {
// Add separator appearance
if (doesClosestXHTMLRequireSeparatorAppearance()) {
final String existingAppearance = attributes.getValue("appearance");
attributes = SAXUtils.addOrReplaceAttribute(attributes, "", "", XFormsConstants.APPEARANCE_QNAME.getName(),
(existingAppearance != null ? existingAppearance + " " : "") + XFormsConstants.XXFORMS_SEPARATOR_APPEARANCE_QNAME.getQualifiedName());
}
// Start xf:repeat
stackElement.startElement(uri, localname, qName, attributes);
// Start xf:repeat-iteration
// NOTE: Use xf:repeat-iteration instead of xxf:iteration so we don't have to deal with a new namespace
reusableAttributes.clear();
reusableAttributes.addAttribute("", "id", "id", XMLReceiverHelper.CDATA, xformsElementId + "~iteration");
final Attributes repeatIterationAttributes = getAttributesGatherNamespaces(uri, qName, reusableAttributes, reusableStringArray, 0);
stackElement.startElement(uri, localname + "-iteration", qName + "-iteration", repeatIterationAttributes);
} else {
// Leave element untouched (except for the id attribute)
stackElement.startElement(uri, localname, qName, attributes);
}
} else {
// Non-XForms element without an XBL binding
String htmlElementId = null;
if (! isRestore) {
if (level == 1) {
if ("head".equals(localname)) {
// Entering head
inHead = true;
} else if ("body".equals(localname)) {
// Entering body
inBody = true;
metadata.initializeBindingLibraryIfNeeded();
}
} else if (level == 2) {
if (inHead && "title".equals(localname)) {
// Entering title
inTitle = true;
// Make sure there will be an id on the title element (ideally, we would do this only if there is a nested xf:output)
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
htmlElementId = reusableStringArray[0];
htmlTitleElementId = htmlElementId;
}
}
}
// NOTE: @id attributes on XHTML elements are rewritten with their effective id during XHTML output by
// XHTMLElementHandler.
if ("true".equals(attributes.getValue(XFormsConstants.XXFORMS_NAMESPACE_URI, "control"))) {
// Non-XForms element which we want to turn into a control (specifically, into a group)
// Create a new xf:group control which specifies the element name to use. Namespace mappings for the
// given QName must be in scope as that QName is the original element name.
final AttributesImpl newAttributes = new AttributesImpl(getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex));
newAttributes.addAttribute(XFormsConstants.XXFORMS_NAMESPACE_URI, "element", "xxf:element", XMLReceiverHelper.CDATA, qName);
startPrefixMapping2("xxf", XFormsConstants.XXFORMS_NAMESPACE_URI);
stackElement.startElement(XFormsConstants.XFORMS_NAMESPACE_URI, "group", "xf:group", newAttributes);
} else if (hostLanguageAVTs) {
// This is a non-XForms element and we allow AVTs
final int attributesCount = attributes.getLength();
if (attributesCount > 0) {
boolean elementWithAVTHasBeenOutput = false;
for (int i = 0; i < attributesCount; i++) {
final String currentAttributeURI = attributes.getURI(i);
if ("".equals(currentAttributeURI) || XMLConstants.XML_URI.equals(currentAttributeURI)) {
// For now we only support AVTs on attributes in no namespace or in the XML namespace (for xml:lang)
final String attributeValue = attributes.getValue(i);
if (XFormsUtils.maybeAVT(attributeValue)) {
// This is an AVT
final String attributeName = attributes.getQName(i);// use qualified name for xml:lang
// Create a new id and update the attributes if needed
if (htmlElementId == null) {
attributes = getAttributesGatherNamespaces(uri, qName, attributes, reusableStringArray, idIndex);
htmlElementId = reusableStringArray[0];
// TODO: Clear all attributes having AVTs or XPath expressions will end up in repeat templates.
}
if (!elementWithAVTHasBeenOutput) {
// Output the element with the new or updated id attribute
stackElement.startElement(uri, localname, qName, attributes);
elementWithAVTHasBeenOutput = true;
}
// Create a new xxf:attribute control
reusableAttributes.clear();
final AttributesImpl newAttributes = (AttributesImpl) getAttributesGatherNamespaces(uri, qName, reusableAttributes, reusableStringArray, -1);
newAttributes.addAttribute("", "for", "for", XMLReceiverHelper.CDATA, htmlElementId);
newAttributes.addAttribute("", "name", "name", XMLReceiverHelper.CDATA, attributeName);
newAttributes.addAttribute("", "value", "value", XMLReceiverHelper.CDATA, attributeValue);
newAttributes.addAttribute("", "for-name", "for-name", XMLReceiverHelper.CDATA, localname);
// These extra attributes can be used alongside src/href attributes
if ("src".equals(attributeName) || "href".equals(attributeName)) {
final String urlType = attributes.getValue(XMLConstants.OPS_FORMATTING_URI, "url-type");
final String portletMode = attributes.getValue(XMLConstants.OPS_FORMATTING_URI, "portlet-mode");
final String windowState = attributes.getValue(XMLConstants.OPS_FORMATTING_URI, "window-state");
if (urlType != null)
newAttributes.addAttribute("", "url-type", "url-type", XMLReceiverHelper.CDATA, urlType);
if (portletMode != null)
newAttributes.addAttribute("", "portlet-mode", "portlet-mode", XMLReceiverHelper.CDATA, "portlet-mode");
if (windowState != null)
newAttributes.addAttribute("", "window-state", "window-state", XMLReceiverHelper.CDATA, "window-state");
}
startPrefixMapping2("xxf", XFormsConstants.XXFORMS_NAMESPACE_URI);
stackElement.element(XFormsConstants.XXFORMS_NAMESPACE_URI, "attribute", "xxf:attribute", newAttributes);
endPrefixMapping2("xxf");
}
}
}
// Output the element as is if no AVT was found
if (!elementWithAVTHasBeenOutput)
stackElement.startElement(uri, localname, qName, attributes);
} else {
stackElement.startElement(uri, localname, qName, attributes);
}
} else {
// No AVT handling, just output the element
stackElement.startElement(uri, localname, qName, attributes);
}
}
}
// Check for preserved content
if (!inPreserve) {
if (inXForms) {
// Preserve as is the content of labels, etc., instances, and schemas
// Within other xf: check for labels, xf:instance, and xs:schema
if (stackElement.isXForms()) {
inLHHA = XFormsConstants.LABEL_HINT_HELP_ALERT_ELEMENT.contains(localname); // labels, etc. may contain XHTML
if (inLHHA || "instance".equals(localname)) { // xf:instance
inPreserve = true;
preserveLevel = level;
}
} else if ("schema".equals(localname) && XMLConstants.XSD_URI.equals(uri)) { // xs:schema
inPreserve = true;
preserveLevel = level;
}
} else {
// At the top-level: check for labels and xbl:xbl
final boolean isXBLXBL = stackElement.isXBL() && "xbl".equals(localname);
if (stackElement.isXForms()) {
inLHHA = XFormsConstants.LABEL_HINT_HELP_ALERT_ELEMENT.contains(localname); // labels, etc. may contain XHTML
if (inLHHA) {
inPreserve = true;
preserveLevel = level;
}
} else if (isXBLXBL) {// xbl:xbl
inPreserve = true;
preserveLevel = level;
inXBL = true;
}
}
}
level++;
}
@Override
public void endElement(String uri, String localname, String qName) throws SAXException {
final StackElement stackElement = endElement();
level--;
if (inPreserve && level == preserveLevel) {
// Leaving preserved content
inPreserve = false;
inLHHA = false;
inXBL = false;
inXBLBinding = false;
}
if (inXForms) {
if (!inPreserve && stackElement.isXForms() && "repeat".equals(localname)) {
// Close xf:repeat-iteration
endElement2(uri, localname + "-iteration", qName + "-iteration");
}
if (level == xformsLevel) {
// Leaving model or controls
inXForms = false;
}
}
if (! isRestore) {
if (level == 1) {
if ("head".equals(localname)) {
// Exiting head
inHead = false;
} else if ("body".equals(localname)) {
// Exiting body
// Add fr:xforms-inspector if requested by property AND if not already present
final PropertySet propertySet = org.orbeon.oxf.properties.Properties.instance().getPropertySet();
final String frURI = "http://orbeon.org/oxf/xml/form-runner";
final String inspectorLocal = "xforms-inspector";
if (propertySet.getBoolean("oxf.epilogue.xforms.inspector", false) && ! metadata.isByNameBindingInUse(frURI, inspectorLocal)) {
// Register the fr:xforms-inspector binding
reusableAttributes.clear();
final scala.Option<IndexableBinding> inspectorBindingOpt =
metadata.findBindingForElement(frURI, inspectorLocal, reusableAttributes);
if (inspectorBindingOpt.isDefined()) {
final String inspectorPrefix = "fr";
final String inspectorQName = XMLUtils.buildQName(inspectorPrefix, inspectorLocal);
reusableAttributes.clear();
reusableAttributes.addAttribute("", "id", "id", XMLReceiverHelper.CDATA, "orbeon-inspector");
final Attributes newAttributes = getAttributesGatherNamespaces(frURI, inspectorQName, reusableAttributes, reusableStringArray, 0);
final String xformsElementId = reusableStringArray[0];
metadata.mapBindingToElement(rewriteId(xformsElementId), inspectorBindingOpt.get());
startPrefixMapping2(inspectorPrefix, frURI);
stackElement.element(frURI, inspectorLocal, inspectorQName, newAttributes);
endPrefixMapping2(inspectorPrefix);
}
}
inBody = false;
}
} else if (level == 2) {
if ("title".equals(localname)) {
// Exiting title
inTitle = false;
}
}
}
// Close element with name that was used to open it
stackElement.endElement();
if (inTitle && stackElement.isXForms() && "output".equals(localname)) {
endPrefixMapping2("xxf");// for resolving appearance
}
namespaceContext.endElement();
}
private boolean hasAVT(Attributes attributes) {
final int attributesCount = attributes.getLength();
if (attributesCount > 0) {
for (int i = 0; i < attributesCount; i++) {
final String currentAttributeURI = attributes.getURI(i);
if ("".equals(currentAttributeURI) || XMLConstants.XML_URI.equals(currentAttributeURI)) {
// For now we only support AVTs on attributes in no namespace or in the XML namespace (for xml:lang)
final String attributeValue = attributes.getValue(i);
if (XFormsUtils.maybeAVT(attributeValue)) {
// This is an AVT
return true;
}
}
}
}
return false;
}
final private void addNamespaceMapping(String rawId) {
final Map<String, String> namespaces = new HashMap<String, String>();
for (Enumeration e = namespaceContext.getPrefixes(); e.hasMoreElements();) {
final String namespacePrefix = (String) e.nextElement();
if (!namespacePrefix.startsWith("xml") && !namespacePrefix.equals("")) {
namespaces.put(namespacePrefix, namespaceContext.getURI(namespacePrefix));
}
}
// Re-add standard "xml" prefix mapping
// TODO: WHY?
namespaces.put(XMLConstants.XML_PREFIX, XMLConstants.XML_URI);
metadata.addNamespaceMapping(rewriteId(rawId), namespaces);
}
final private void putMark(String rawId) {
metadata.putMark(templateSAXStore.getMark(rewriteId(rawId)));
}
protected String rewriteId(String id) {
return id;
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
namespaceContext.startPrefixMapping(prefix, uri);
startPrefixMapping2(prefix, uri);
}
private Attributes getAttributesGatherNamespaces(String uriForDebug, String qNameForDebug, Attributes attributes, String[] newIdAttribute, final int idIndex) {
final String rawId;
if (isGenerateIds) {
// Process ids
if (idIndex == -1) {
// Create a new "id" attribute, prefixing if needed
final AttributesImpl newAttributes = new AttributesImpl(attributes);
rawId = metadata.idGenerator().nextId();
newAttributes.addAttribute("", "id", "id", XMLReceiverHelper.CDATA, rawId);
attributes = newAttributes;
} else {
// Keep existing id
rawId = attributes.getValue(idIndex);
// Check for duplicate ids
// See https://github.com/orbeon/orbeon-forms/issues/1892
// TODO: create Element to provide more location info?
if (isTopLevel && metadata.idGenerator().contains(rawId))
throw new ValidationException("Duplicate id for XForms element: " + rawId,
new ExtendedLocationData(LocationData.createIfPresent(documentLocator()), "analyzing control element",
new String[] { "element", SAXUtils.saxElementToDebugString(uriForDebug, qNameForDebug, attributes), "id", rawId }));
}
} else {
// Don't create a new id but remember the existing one
rawId = attributes.getValue(idIndex);
}
// Remember that this id was used
if (rawId != null) {
metadata.idGenerator().add(rawId);
// Gather namespace information if there is an id
if (isGenerateIds || idIndex != -1) {
addNamespaceMapping(rawId);
}
}
newIdAttribute[0] = rawId;
return attributes;
}
private static final scala.Option<IndexableBinding> NONE_INDEXABLE_BINDING = scala.Option.apply(null);
}