/**
* 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;
import org.apache.commons.lang3.StringUtils;
import org.ccil.cowan.tagsoup.HTMLSchema;
import org.orbeon.dom.*;
import org.orbeon.dom.io.DocumentSource;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.pipeline.api.TransformerXMLReceiver;
import org.orbeon.oxf.processor.DebugProcessor;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.util.SecureUtils;
import org.orbeon.oxf.util.URLRewriterUtils;
import org.orbeon.oxf.util.XPathCache;
import org.orbeon.oxf.xforms.analysis.controls.LHHAAnalysis;
import org.orbeon.oxf.xforms.control.controls.XFormsOutputControl;
import org.orbeon.oxf.xforms.control.controls.XXFormsAttributeControl;
import org.orbeon.oxf.xforms.model.DataModel;
import org.orbeon.oxf.xforms.model.InstanceData;
import org.orbeon.oxf.xforms.state.ControlState;
import org.orbeon.oxf.xforms.state.WhitelistObjectInputStream;
import org.orbeon.oxf.xforms.xbl.Scope;
import org.orbeon.oxf.xforms.xbl.XBLContainer;
import org.orbeon.oxf.xml.*;
import org.orbeon.oxf.xml.XMLUtils;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.oxf.xml.dom4j.LocationDocumentResult;
import org.orbeon.oxf.xml.dom4j.LocationDocumentSource;
import org.orbeon.saxon.om.*;
import org.orbeon.saxon.value.*;
import org.orbeon.saxon.value.StringValue;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.TransformerHandler;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
public class XFormsUtils {
public static String encodeXMLAsDOM(org.w3c.dom.Node node) {
try {
return encodeXML(TransformerUtils.domToDom4jDocument(node), XFormsProperties.isGZIPState(), true, false);
} catch (TransformerException e) {
throw new OXFException(e);
}
}
public static String encodeXML(Document documentToEncode, boolean encodeLocationData) {
return encodeXML(documentToEncode, XFormsProperties.isGZIPState(), true, encodeLocationData);
}
// 2016-09-14: `encrypt = false` only when encoding static state when using server-side state handling.
public static String encodeXML(Document document, boolean compress, boolean encrypt, boolean location) {
// XFormsServer.logger.debug("XForms - encoding XML.");
// Get SAXStore
// TODO: This is not optimal since we create a second in-memory representation. Should stream instead.
final SAXStore saxStore = new SAXStore();
// NOTE: We don't encode XML comments and use only the ContentHandler interface
final Source source = location ? new LocationDocumentSource(document) : new DocumentSource(document);
TransformerUtils.sourceToSAX(source, saxStore);
// Serialize SAXStore to bytes
// TODO: This is not optimal since we create a third in-memory representation. Should stream instead.
final byte[] bytes;
try {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
saxStore.writeExternal(new ObjectOutputStream(byteArrayOutputStream));
bytes = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new OXFException(e);
}
// Encode bytes
return encodeBytes(bytes, compress, encrypt);
}
// 2016-09-14: `encrypt = false` only when encoding static state when using server-side state handling, and
// for some unit tests.
public static String encodeBytes(byte[] bytesToEncode, boolean compress, boolean encrypt) {
// Compress if needed
final byte[] gzipByteArray = compress ? XFormsCompressor.compressBytes(bytesToEncode) : null;
// Encrypt if needed
if (encrypt) {
// Perform encryption
if (gzipByteArray == null) {
// The data was not compressed above
return "X1" + SecureUtils.encrypt(bytesToEncode);
} else {
// The data was compressed above
return "X2" + SecureUtils.encrypt(gzipByteArray);
}
} else {
// No encryption
if (gzipByteArray == null) {
// The data was not compressed above
return "X3" + org.orbeon.oxf.util.Base64.encode(bytesToEncode, false);
} else {
// The data was compressed above
return "X4" + org.orbeon.oxf.util.Base64.encode(gzipByteArray, false);
}
}
}
private static final HTMLSchema TAGSOUP_HTML_SCHEMA = new HTMLSchema();
private static void htmlStringToResult(String value, LocationData locationData, Result result) {
try {
final XMLReader xmlReader = new org.ccil.cowan.tagsoup.Parser();
xmlReader.setProperty(org.ccil.cowan.tagsoup.Parser.schemaProperty, TAGSOUP_HTML_SCHEMA);
xmlReader.setFeature(org.ccil.cowan.tagsoup.Parser.ignoreBogonsFeature, true);
final TransformerHandler identity = TransformerUtils.getIdentityTransformerHandler();
identity.setResult(result);
xmlReader.setContentHandler(identity);
final InputSource inputSource = new InputSource();
inputSource.setCharacterStream(new StringReader(value));
xmlReader.parse(inputSource);
} catch (Exception e) {
throw new ValidationException("Cannot parse value as text/html for value: '" + value + "'", locationData);
}
// r.setFeature(Parser.CDATAElementsFeature, false);
// r.setFeature(Parser.namespacesFeature, false);
// r.setFeature(Parser.ignoreBogonsFeature, true);
// r.setFeature(Parser.bogonsEmptyFeature, false);
// r.setFeature(Parser.defaultAttributesFeature, false);
// r.setFeature(Parser.translateColonsFeature, true);
// r.setFeature(Parser.restartElementsFeature, false);
// r.setFeature(Parser.ignorableWhitespaceFeature, true);
// r.setProperty(Parser.scannerProperty, new PYXScanner());
// r.setProperty(Parser.lexicalHandlerProperty, h);
}
public static org.w3c.dom.Document htmlStringToDocumentTagSoup(String value, LocationData locationData) {
final org.w3c.dom.Document document = XMLParsing.createDocument();
final DOMResult domResult = new DOMResult(document);
htmlStringToResult(value, locationData, domResult);
return document;
}
public static Document htmlStringToDom4jTagSoup(String value, LocationData locationData) {
final LocationDocumentResult documentResult = new LocationDocumentResult();
htmlStringToResult(value, locationData, documentResult);
return documentResult.getDocument();
}
// TODO: implement server-side plain text output with <br> insertion
// public static void streamPlainText(final ContentHandler contentHandler, String value, LocationData locationData, final String xhtmlPrefix) {
// // 1: Split string along 0x0a and remove 0x0d (?)
// // 2: Output string parts, and between them, output <xhtml:br> element
// try {
// contentHandler.characters(filteredValue.toCharArray(), 0, filteredValue.length());
// } catch (SAXException e) {
// throw new OXFException(e);
// }
// }
public static void streamHTMLFragment(XMLReceiver xmlReceiver, String value, LocationData locationData, String xhtmlPrefix) {
if (StringUtils.isNotBlank(value)) { // don't parse blank values
final org.w3c.dom.Document htmlDocument = htmlStringToDocumentTagSoup(value, locationData);
// Stream fragment to the output
if (htmlDocument != null) {
TransformerUtils.sourceToSAX(new DOMSource(htmlDocument), new HTMLBodyXMLReceiver(xmlReceiver, xhtmlPrefix));
}
}
}
/**
* Get the value of a child element known to have only static content.
*
* @param childElement element to evaluate (xf:label, etc.)
* @param acceptHTML whether the result may contain HTML
* @param containsHTML whether the result actually contains HTML (null allowed)
* @return string containing the result of the evaluation, null if evaluation failed
*/
public static String getStaticChildElementValue(final String prefix, final Element childElement, final boolean acceptHTML, final boolean[] containsHTML) {
// Check that there is a current child element
if (childElement == null)
return null;
// No HTML found by default
if (containsHTML != null)
containsHTML[0] = false;
// Try to get inline value
{
final StringBuilder sb = new StringBuilder(20);
// Visit the subtree and serialize
// NOTE: It is a little funny to do our own serialization here, but the alternative is to build a DOM and
// serialize it, which is not trivial because of the possible interleaved xf:output's. Furthermore, we
// perform a very simple serialization of elements and text to simple (X)HTML, not full-fledged HTML or XML
// serialization.
Dom4jUtils.visitSubtree(childElement, new LHHAElementVisitorListener(prefix, acceptHTML, containsHTML, sb, childElement));
if (acceptHTML && containsHTML != null && !containsHTML[0]) {
// We went through the subtree and did not find any HTML
// If the caller supports the information, return a non-escaped string so we can optimize output later
return XMLUtils.unescapeXMLMinimal(sb.toString());
} else {
// We found some HTML, just return it
return sb.toString();
}
}
}
/**
* Get the value of a child element by pushing the context of the child element on the binding stack first, then
* calling getElementValue() and finally popping the binding context.
*
* @param container current XFormsContainingDocument
* @param sourceEffectiveId source effective id for id resolution
* @param scope XBL scope
* @param childElement element to evaluate (xf:label, etc.)
* @param acceptHTML whether the result may contain HTML
* @param containsHTML whether the result actually contains HTML (null allowed)
* @return string containing the result of the evaluation, null if evaluation failed
*/
public static String getChildElementValue(final XBLContainer container, final String sourceEffectiveId,
final Scope scope, final Element childElement, final boolean acceptHTML,
final boolean defaultHTML, boolean[] containsHTML) {
// Check that there is a current child element
if (childElement == null)
return null;
// Child element becomes the new binding
final XFormsContextStack contextStack = container.getContextStack();
contextStack.pushBinding(childElement, sourceEffectiveId, scope);
final String result = getElementValue(container, contextStack, sourceEffectiveId, childElement, acceptHTML, defaultHTML, containsHTML);
contextStack.popBinding();
return result;
}
/**
* Get the value of an element by trying single-node binding, value attribute, linking attribute, and inline value
* (including nested XHTML and xf:output elements).
*
* This may return an HTML string if HTML is accepted and found, or a plain string otherwise.
*
* @param container current XBLContainer
* @param contextStack context stack for XPath evaluation
* @param sourceEffectiveId source effective id for id resolution
* @param childElement element to evaluate (xf:label, etc.)
* @param acceptHTML whether the result may contain HTML
* @param containsHTML whether the result actually contains HTML (null allowed)
* @return string containing the result of the evaluation, null if evaluation failed (see comments)
*/
public static String getElementValue(final XBLContainer container,
final XFormsContextStack contextStack, final String sourceEffectiveId,
final Element childElement, final boolean acceptHTML, final boolean defaultHTML,
final boolean[] containsHTML) {
// No HTML found by default
if (containsHTML != null)
containsHTML[0] = defaultHTML;
final BindingContext currentBindingContext = contextStack.getCurrentBindingContext();
// "the order of precedence is: single node binding attributes, linking attributes, inline text."
// Try to get single node binding
{
final boolean hasSingleNodeBinding = currentBindingContext.newBind();
if (hasSingleNodeBinding) {
final Item boundItem = currentBindingContext.getSingleItem();
final String tempResult = DataModel.getValue(boundItem);
if (tempResult != null) {
return (acceptHTML && containsHTML == null) ? XMLUtils.escapeXMLMinimal(tempResult) : tempResult;
} else {
// There is a single-node binding but it doesn't point to an acceptable item
return null;
}
}
}
// Try to get value attribute
// NOTE: This is an extension attribute not standard in XForms 1.0 or 1.1
{
final String valueAttribute = childElement.attributeValue(XFormsConstants.VALUE_QNAME);
final boolean hasValueAttribute = valueAttribute != null;
if (hasValueAttribute) {
final List<Item> currentNodeset = currentBindingContext.nodeset();
if (currentNodeset != null && currentNodeset.size() > 0) {
String tempResult;
try {
tempResult = XPathCache.evaluateAsString(
currentNodeset,
currentBindingContext.position(),
valueAttribute,
container.getNamespaceMappings(childElement),
contextStack.getCurrentBindingContext().getInScopeVariables(),
container.getContainingDocument().getFunctionLibrary(),
contextStack.getFunctionContext(sourceEffectiveId),
null,
(LocationData) childElement.getData(),
container.getContainingDocument().getRequestStats().getReporter()
);
} catch (Exception e) {
XFormsError.handleNonFatalXPathError(container, e);
tempResult = "";
}
return (acceptHTML && containsHTML == null) ? XMLUtils.escapeXMLMinimal(tempResult) : tempResult;
} else {
// There is a value attribute but the evaluation context is empty
return null;
}
}
}
// NOTE: Linking attribute is deprecated in XForms 1.1 and we no longer support it.
// Try to get inline value
{
final StringBuilder sb = new StringBuilder(20);
// Visit the subtree and serialize
// NOTE: It is a little funny to do our own serialization here, but the alternative is to build a DOM and
// serialize it, which is not trivial because of the possible interleaved xf:output's. Furthermore, we
// perform a very simple serialization of elements and text to simple (X)HTML, not full-fledged HTML or XML
// serialization.
Dom4jUtils.visitSubtree(childElement, new LHHAElementVisitorListener(container, contextStack,
sourceEffectiveId, acceptHTML, defaultHTML, containsHTML, sb, childElement));
if (acceptHTML && containsHTML != null && !containsHTML[0]) {
// We went through the subtree and did not find any HTML
// If the caller supports the information, return a non-escaped string so we can optimize output later
return XMLUtils.unescapeXMLMinimal(sb.toString());
} else {
// We found some HTML, just return it
return sb.toString();
}
}
}
public static ValueRepresentation convertJavaObjectToSaxonObject(Object object) {
final ValueRepresentation valueRepresentation;
if (object instanceof ValueRepresentation) {
// Native Saxon variable value
valueRepresentation = (ValueRepresentation) object;
} else if (object instanceof String) {
valueRepresentation = new StringValue((String) object);
} else if (object instanceof Boolean) {
valueRepresentation = BooleanValue.get((Boolean) object);
} else if (object instanceof Integer) {
valueRepresentation = new Int64Value((Integer) object);
} else if (object instanceof Float) {
valueRepresentation = new FloatValue((Float) object);
} else if (object instanceof Double) {
valueRepresentation = new DoubleValue((Double) object);
} else if (object instanceof URI) {
valueRepresentation = new AnyURIValue(object.toString());
} else {
throw new OXFException("Invalid variable type: " + object.getClass());
}
return valueRepresentation;
}
public static Document decodeXML(String encodedXML, boolean forceEncryption) {
final byte[] bytes = decodeBytes(encodedXML, forceEncryption);
// Deserialize bytes to SAXStore
// TODO: This is not optimal
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
final SAXStore saxStore = new SAXStore(WhitelistObjectInputStream.apply(byteArrayInputStream, SAXStore.class));
// Deserialize SAXStore to dom4j document
// TODO: This is not optimal
final TransformerXMLReceiver identity = TransformerUtils.getIdentityTransformerHandler();
final LocationDocumentResult result = new LocationDocumentResult();
identity.setResult(result);
try {
saxStore.replay(identity);
} catch (SAXException e) {
throw new OXFException(e);
}
return result.getDocument();
}
public static byte[] decodeBytes(String encoded, boolean forceEncryption) {
// Get raw text
byte[] resultBytes;
{
final String prefix = encoded.substring(0, 2);
final String encodedString = encoded.substring(2);
final byte[] resultBytes1;
final byte[] gzipByteArray;
if (prefix.equals("X1")) {
// Encryption + uncompressed
resultBytes1 = SecureUtils.decrypt(encodedString);
gzipByteArray = null;
} else if (prefix.equals("X2")) {
// Encryption + compressed
resultBytes1 = null;
gzipByteArray = SecureUtils.decrypt(encodedString);
} else if (! forceEncryption && prefix.equals("X3")) {
// No encryption + uncompressed
resultBytes1 = org.orbeon.oxf.util.Base64.decode(encodedString);
gzipByteArray = null;
} else if (! forceEncryption && prefix.equals("X4")) {
// No encryption + compressed
resultBytes1 = null;
gzipByteArray = org.orbeon.oxf.util.Base64.decode(encodedString);
} else {
throw new OXFException("Invalid prefix for encoded string: " + prefix);
}
// Decompress if needed
if (gzipByteArray != null) {
resultBytes = XFormsCompressor.uncompressBytes(gzipByteArray);
} else {
resultBytes = resultBytes1;
}
}
return resultBytes;
}
public static String resolveRenderURL(XFormsContainingDocument containingDocument, Element currentElement, String url, boolean skipRewrite) {
final URI resolvedURI = resolveXMLBase(containingDocument, currentElement, url);
final String resolvedURIStringNoPortletFragment = uriToStringRemoveFragmentForPortletAndEmbedded(containingDocument, resolvedURI);
return skipRewrite ? resolvedURIStringNoPortletFragment :
NetUtils.getExternalContext().getResponse().rewriteRenderURL(resolvedURIStringNoPortletFragment, null, null);
}
public static String resolveActionURL(XFormsContainingDocument containingDocument, Element currentElement, String url) {
final URI resolvedURI = resolveXMLBase(containingDocument, currentElement, url);
final String resolvedURIStringNoPortletFragment = uriToStringRemoveFragmentForPortletAndEmbedded(containingDocument, resolvedURI);
return NetUtils.getExternalContext().getResponse().rewriteActionURL(resolvedURIStringNoPortletFragment, null, null);
}
private static String uriToStringRemoveFragmentForPortletAndEmbedded(XFormsContainingDocument containingDocument, URI resolvedURI) {
if ((containingDocument.isPortletContainer() || containingDocument.isEmbedded()) && resolvedURI.getFragment() != null) {
// Page was loaded from a portlet or embedding API and there is a fragment, remove it
try {
return new URI(resolvedURI.getScheme(), resolvedURI.getRawAuthority(), resolvedURI.getRawPath(), resolvedURI.getRawQuery(), null).toString();
} catch (URISyntaxException e) {
throw new OXFException(e);
}
} else {
return resolvedURI.toString();
}
}
/**
* Resolve a resource URL including xml:base resolution.
*
*
* @param containingDocument current document
* @param element element used to start resolution (if null, no resolution takes place)
* @param url URL to resolve
* @param rewriteMode rewrite mode (see ExternalContext.Response)
* @return resolved URL
*/
public static String resolveResourceURL(XFormsContainingDocument containingDocument, Element element, String url, int rewriteMode) {
final URI resolvedURI = resolveXMLBase(containingDocument, element, url);
return NetUtils.getExternalContext().getResponse().rewriteResourceURL(resolvedURI.toString(), rewriteMode);
}
/**
* Resolve a resource URL including xml:base resolution.
*
* @param containingDocument current document
* @param element element used to start resolution (if null, no resolution takes place)
* @param url URL to resolve
* @param rewriteMode rewrite mode (see ExternalContext.Response)
* @return resolved URL
*/
public static String resolveServiceURL(XFormsContainingDocument containingDocument, Element element, String url, int rewriteMode) {
final URI resolvedURI = resolveXMLBase(containingDocument, element, url);
return URLRewriterUtils.rewriteServiceURL(NetUtils.getExternalContext().getRequest(), resolvedURI.toString(), rewriteMode);
}
/**
* Resolve attribute value templates (AVTs).
*
* @param xpathContext current XPath context
* @param contextNode context node for evaluation
* @param attributeValue attribute value
* @return resolved attribute value
*/
public static String resolveAttributeValueTemplates(XFormsContainingDocument containingDocument, XPathCache.XPathContext xpathContext, NodeInfo contextNode, String attributeValue) {
if (attributeValue == null)
return null;
return XPathCache.evaluateAsAvt(xpathContext, contextNode, attributeValue, containingDocument.getRequestStats().getReporter());
}
/**
* Resolve a URI string against an element, taking into account ancestor xml:base attributes for
* the resolution, and using the document's request URL as a base.
*
* @param containingDocument current document
* @param element element used to start resolution (if null, no resolution takes place)
* @param uri URI to resolve
* @return resolved URI
*/
public static URI resolveXMLBase(XFormsContainingDocument containingDocument, Element element, String uri) {
return resolveXMLBase(element, containingDocument.getRequestPath(), uri);
}
/**
* Resolve a URI string against an element, taking into account ancestor xml:base attributes for
* the resolution.
*
* @param element element used to start resolution (if null, no resolution takes place)
* @param baseURI optional base URI
* @param uri URI to resolve
* @return resolved URI
*/
public static URI resolveXMLBase(Element element, String baseURI, String uri) {
try {
// Allow for null Element
if (element == null)
return new URI(uri);
final List<String> xmlBaseValues = new ArrayList<String>();
// Collect xml:base values
Element currentElement = element;
do {
final String xmlBaseAttribute = currentElement.attributeValue(XMLConstants.XML_BASE_QNAME);
if (xmlBaseAttribute != null)
xmlBaseValues.add(xmlBaseAttribute);
currentElement = currentElement.getParent();
} while(currentElement != null);
// Append base if needed
if (baseURI != null)
xmlBaseValues.add(baseURI);
// Go from root to leaf
Collections.reverse(xmlBaseValues);
xmlBaseValues.add(uri);
// Resolve paths from root to leaf
URI result = null;
for (final String currentXMLBase: xmlBaseValues) {
final URI currentXMLBaseURI = new URI(currentXMLBase);
result = (result == null) ? currentXMLBaseURI : result.resolve(currentXMLBaseURI);
}
return result;
} catch (URISyntaxException e) {
throw new ValidationException("Error while resolving URI: " + uri, e, (element != null) ? (LocationData) element.getData() : null);
}
}
/**
* Resolve f:url-norewrite attributes on this element, taking into account ancestor f:url-norewrite attributes for
* the resolution.
*
* @param element element used to start resolution
* @return true if rewriting is turned off, false otherwise
*/
public static boolean resolveUrlNorewrite(Element element) {
Element currentElement = element;
do {
final String urlNorewriteAttribute = currentElement.attributeValue(XMLConstants.FORMATTING_URL_NOREWRITE_QNAME);
// Return the first ancestor value found
if (urlNorewriteAttribute != null)
return "true".equals(urlNorewriteAttribute);
currentElement = currentElement.getParent();
} while(currentElement != null);
// Default is to rewrite
return false;
}
/**
* Log a message and Document.
*
* @param debugMessage the message to display
* @param document the Document to display
*/
public static void logDebugDocument(String debugMessage, Document document) {
DebugProcessor.logger.info(debugMessage + ":\n" + Dom4jUtils.domToPrettyString(document));
}
/**
* Prefix an id with the container namespace if needed. If the id is null, return null.
*
* @param containingDocument current ContainingDocument
* @param id id to prefix
* @return prefixed id or null
*/
public static String namespaceId(XFormsContainingDocument containingDocument, CharSequence id) {
if (id == null)
return null;
else
return containingDocument.getContainerNamespace() + id;
}
/**
* Remove the container namespace prefix if possible. If the id is null, return null.
*
* @param containingDocument current ContainingDocument
* @param id id to de-prefix
* @return de-prefixed id if possible or null
*/
public static String deNamespaceId(XFormsContainingDocument containingDocument, String id) {
if (id == null)
return null;
final String containerNamespace = containingDocument.getContainerNamespace();
if (containerNamespace.length() > 0 && id.startsWith(containerNamespace))
return id.substring(containerNamespace.length());
else
return id;
}
/**
* Return LocationData for a given node, null if not found.
*
* @param node node containing the LocationData
* @return LocationData or null
*/
public static LocationData getNodeLocationData(Node node) {
final Object data;
{
if (node instanceof Element)
data = ((Element) node).getData();
else if (node instanceof Attribute)
data = ((Attribute) node).getData();
else
data = null;
// TODO: other node types
}
if (data == null)
return null;
else if (data instanceof LocationData)
return (LocationData) data;
else if (data instanceof InstanceData)
return ((InstanceData) data).getLocationData();
return null;
}
/**
* Return the underlying Node from the given NodeInfo, possibly converting it to a Dom4j Node. Changes to the returned Node may or may not
* reflect on the original, depending on its type.
*
* @param nodeInfo NodeInfo to process
* @return Node
*/
public static Node getNodeFromNodeInfoConvert(NodeInfo nodeInfo) {
if (nodeInfo instanceof VirtualNode)
return (Node) ((VirtualNode) nodeInfo).getUnderlyingNode();
else if (nodeInfo.getNodeKind() == org.w3c.dom.Node.ATTRIBUTE_NODE) {
return DocumentFactory.createAttribute(
null,
QName.get(nodeInfo.getLocalPart(), Namespace$.MODULE$.apply(nodeInfo.getPrefix(), nodeInfo.getURI())),
nodeInfo.getStringValue()
);
} else
return TransformerUtils.tinyTreeToDom4j((nodeInfo.getParent() instanceof DocumentInfo) ? nodeInfo.getParent() : nodeInfo);
}
/**
* Return the underlying Node from the given NodeInfo if possible. If not, throw an exception with the given error
* message.
*
* @param nodeInfo NodeInfo to process
* @param errorMessage error message to throw
* @return Node if found
*/
public static Node getNodeFromNodeInfo(NodeInfo nodeInfo, String errorMessage) {
if (!(nodeInfo instanceof VirtualNode))
throw new OXFException(errorMessage);
return (Node) ((VirtualNode) nodeInfo).getUnderlyingNode();
}
private static String[] voidElementsNames = {
// HTML 5: http://www.w3.org/TR/html5/syntax.html#void-elements
"area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr",
// Legacy
"basefont", "frame", "isindex"
};
private static final Set<String> voidElements = new HashSet<String>(Arrays.asList(voidElementsNames));
public static boolean isVoidElement(String elementName) {
return voidElements.contains(elementName);
}
private static class LHHAElementVisitorListener implements Dom4jUtils.VisitorListener {
private final String prefix;
private final XBLContainer container;
private final XFormsContextStack contextStack;
private final String sourceEffectiveId;
private final boolean acceptHTML;
private final boolean defaultHTML;
private final boolean[] containsHTML;
private final StringBuilder sb;
private final Element childElement;
private final boolean hostLanguageAVTs;
// Constructor for "static" case, i.e. when we know the child element cannot have dynamic content
public LHHAElementVisitorListener(String prefix, boolean acceptHTML, boolean[] containsHTML, StringBuilder sb, Element childElement) {
this.prefix = prefix;
this.container = null;
this.contextStack = null;
this.sourceEffectiveId = null;
this.acceptHTML = acceptHTML;
this.defaultHTML = false;
this.containsHTML = containsHTML;
this.sb = sb;
this.childElement = childElement;
this.hostLanguageAVTs = false;
}
// Constructor for "dynamic" case, i.e. when we know the child element can have dynamic content
public LHHAElementVisitorListener(XBLContainer container, XFormsContextStack contextStack,
String sourceEffectiveId, boolean acceptHTML, boolean defaultHTML, boolean[] containsHTML,
StringBuilder sb, Element childElement) {
this.prefix = container.getFullPrefix();
this.container = container;
this.contextStack = contextStack;
this.sourceEffectiveId = sourceEffectiveId;
this.acceptHTML = acceptHTML;
this.defaultHTML = defaultHTML;
this.containsHTML = containsHTML;
this.sb = sb;
this.childElement = childElement;
this.hostLanguageAVTs = XFormsProperties.isHostLanguageAVTs();
}
private boolean lastIsStart = false;
public void startElement(Element element) {
if (element.getQName().equals(XFormsConstants.XFORMS_OUTPUT_QNAME)) {
// This is an xf:output nested among other markup
final XFormsOutputControl outputControl = new XFormsOutputControl(container, null, element, null) {
// Override this as super.getContextStack() gets the containingDocument's stack, and here we need whatever is the current stack
// Probably need to modify super.getContextStack() at some point to NOT use the containingDocument's stack
@Override
public XFormsContextStack getContextStack() {
return LHHAElementVisitorListener.this.contextStack;
}
@Override
public String getEffectiveId() {
// Return given source effective id, so we have a source effective id for resolution of index(), etc.
return sourceEffectiveId;
}
@Override
public boolean isAllowedBoundItem(Item item) {
return DataModel.isAllowedBoundItem(item);
}
};
final boolean isHTMLMediatype =
! defaultHTML && LHHAAnalysis.isHTML(element) || defaultHTML && ! LHHAAnalysis.isPlainText(element);
contextStack.pushBinding(element, sourceEffectiveId, outputControl.getChildElementScope(element));
outputControl.setBindingContext(contextStack.getCurrentBindingContext(), true, false, false, scala.Option.<ControlState>apply(null));
contextStack.popBinding();
if (outputControl.isRelevant()) {
if (acceptHTML) {
if (isHTMLMediatype) {
if (containsHTML != null)
containsHTML[0] = true; // this indicates for sure that there is some nested HTML
sb.append(outputControl.getExternalValue());
} else {
// Mediatype is not HTML so we don't escape
sb.append(XMLUtils.escapeXMLMinimal(outputControl.getExternalValue()));
}
} else {
if (isHTMLMediatype) {
// HTML is not allowed here, better tell the user
throw new OXFException("HTML not allowed within element: " + childElement.getName());
} else {
// Mediatype is not HTML so we don't escape
sb.append(outputControl.getExternalValue());
}
}
}
} else {
// This is a regular element, just serialize the start tag to no namespace
// If HTML is not allowed here, better tell the user
if (!acceptHTML)
throw new OXFException("Nested XHTML or XForms not allowed within element: " + childElement.getName());
if (containsHTML != null)
containsHTML[0] = true;// this indicates for sure that there is some nested HTML
sb.append('<');
sb.append(element.getName());
final List attributes = element.attributes();
if (attributes.size() > 0) {
for (Object attribute: attributes) {
final Attribute currentAttribute = (Attribute) attribute;
final String currentAttributeName = currentAttribute.getName();
final String currentAttributeValue = currentAttribute.getValue();
final String resolvedValue;
if (hostLanguageAVTs && maybeAVT(currentAttributeValue)) {
// This is an AVT, use attribute control to produce the output
final XXFormsAttributeControl attributeControl
= new XXFormsAttributeControl(container, element, currentAttributeName, currentAttributeValue, element.getName());
contextStack.pushBinding(element, sourceEffectiveId, attributeControl.getChildElementScope(element));
attributeControl.setBindingContext(contextStack.getCurrentBindingContext(), true, false, false, scala.Option.<ControlState >apply(null));
contextStack.popBinding();
resolvedValue = attributeControl.getExternalValue();
} else if (currentAttributeName.equals("id")) {
// This is an id, prefix if needed
resolvedValue = prefix + currentAttributeValue;
} else {
// Simply use control value
resolvedValue = currentAttributeValue;
}
// Only consider attributes in no namespace
if ("".equals(currentAttribute.getNamespaceURI())) {
sb.append(' ');
sb.append(currentAttributeName);
sb.append("=\"");
if (resolvedValue != null)
sb.append(XMLUtils.escapeXMLMinimal(resolvedValue));
sb.append('"');
}
}
}
sb.append('>');
lastIsStart = true;
}
}
public void endElement(Element element) {
final String elementName = element.getName();
if ((!lastIsStart || !isVoidElement(elementName)) && !element.getQName().equals(XFormsConstants.XFORMS_OUTPUT_QNAME)) {
// This is a regular element, just serialize the end tag to no namespace
// UNLESS the element was just opened. This means we output <br>, not <br></br>, etc.
sb.append("</");
sb.append(elementName);
sb.append('>');
}
lastIsStart = false;
}
public void text(Text text) {
sb.append(acceptHTML ? XMLUtils.escapeXMLMinimal(text.getStringValue()) : text.getStringValue());
lastIsStart = false;
}
}
public static String escapeJavaScript(String value) {
return StringUtils.replace(StringUtils.replace(StringUtils.replace(value, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n");
}
public static boolean maybeAVT(String attributeValue) {
return attributeValue.indexOf('{') != -1;
}
/**
* Return the prefix of an effective id, e.g. "" or "foo$bar$". The prefix returned does end with a separator.
*
* @param effectiveId effective id to check
* @return prefix if any, "" if none, null if effectiveId was null
*/
public static String getEffectiveIdPrefix(String effectiveId) {
if (effectiveId == null)
return null;
final int prefixIndex = effectiveId.lastIndexOf(XFormsConstants.COMPONENT_SEPARATOR);
if (prefixIndex != -1) {
return effectiveId.substring(0, prefixIndex + 1);
} else {
return "";
}
}
/**
* Return whether the effective id has a suffix.
*
* @param effectiveId effective id to check
* @return true iif the effective id has a suffix
*/
public static boolean hasEffectiveIdSuffix(String effectiveId) {
return (effectiveId != null) && effectiveId.indexOf(XFormsConstants.REPEAT_SEPARATOR) != -1;
}
/**
* Return the suffix of an effective id, e.g. "" or "2-5-1". The suffix returned does not start with a separator.
*
* @param effectiveId effective id to check
* @return suffix if any, "" if none, null if effectiveId was null
*/
public static String getEffectiveIdSuffix(String effectiveId) {
if (effectiveId == null)
return null;
final int suffixIndex = effectiveId.indexOf(XFormsConstants.REPEAT_SEPARATOR);
if (suffixIndex != -1) {
return effectiveId.substring(suffixIndex + 1);
} else {
return "";
}
}
/**
* Return the suffix of an effective id, e.g. "" or ".2-5-1". The suffix returned starts with a separator.
*
* @param effectiveId effective id to check
* @return suffix if any, "" if none, null if effectiveId was null
*/
public static String getEffectiveIdSuffixWithSeparator(String effectiveId) {
if (effectiveId == null)
return null;
final int suffixIndex = effectiveId.indexOf(XFormsConstants.REPEAT_SEPARATOR);
if (suffixIndex != -1) {
return effectiveId.substring(suffixIndex);
} else {
return "";
}
}
/**
* Return an effective id's prefixed id, i.e. the effective id without its suffix, e.g.:
*
* o foo$bar$my-input.1-2 => foo$bar$my-input
*
* @param effectiveId effective id to check
* @return effective id without its suffix, null if effectiveId was null
*/
public static String getPrefixedId(String effectiveId) {
if (effectiveId == null)
return null;
final int suffixIndex = effectiveId.indexOf(XFormsConstants.REPEAT_SEPARATOR);
if (suffixIndex != -1) {
return effectiveId.substring(0, suffixIndex);
} else {
return effectiveId;
}
}
/**
* Return an effective id without its prefix, e.g.:
*
* o foo$bar$my-input => my-input
* o foo$bar$my-input.1-2 => my-input.1-2
*
* @param effectiveId effective id to check
* @return effective id without its prefix, null if effectiveId was null
*/
public static String getEffectiveIdNoPrefix(String effectiveId) {
if (effectiveId == null)
return null;
final int prefixIndex = effectiveId.lastIndexOf(XFormsConstants.COMPONENT_SEPARATOR);
if (prefixIndex != -1) {
return effectiveId.substring(prefixIndex + 1);
} else {
return effectiveId;
}
}
/**
* Return the parts of an effective id prefix, e.g. for foo$bar$my-input return new String[] { "foo", "bar" }
*
* @param effectiveId effective id to check
* @return array of parts, empty array if no parts, null if effectiveId was null
*/
public static String[] getEffectiveIdPrefixParts(String effectiveId) {
if (effectiveId == null)
return null;
final int prefixIndex = effectiveId.lastIndexOf(XFormsConstants.COMPONENT_SEPARATOR);
if (prefixIndex != -1) {
return StringUtils.split(effectiveId.substring(0, prefixIndex), XFormsConstants.COMPONENT_SEPARATOR);
} else {
return EMPTY_STRING_ARRAY;
}
}
private static String[] EMPTY_STRING_ARRAY = new String[0];
/**
* Given a repeat control's effective id, compute the effective id of an iteration.
*
* @param repeatEffectiveId repeat control effective id
* @param iterationIndex repeat iteration
* @return repeat iteration effective id
*/
public static String getIterationEffectiveId(String repeatEffectiveId, int iterationIndex) {
final String parentSuffix = XFormsUtils.getEffectiveIdSuffixWithSeparator(repeatEffectiveId);
final String iterationPrefixedId = getPrefixedId(repeatEffectiveId) + "~iteration";
if (parentSuffix.equals("")) {
// E.g. foobar => foobar~iteration.3
return iterationPrefixedId + XFormsConstants.REPEAT_SEPARATOR + iterationIndex;
} else {
// E.g. foobar.3-7 => foobar~iteration.3-7-2
return iterationPrefixedId + parentSuffix + XFormsConstants.REPEAT_INDEX_SEPARATOR + iterationIndex;
}
}
/**
* Return the parts of an effective id suffix, e.g. for $foo$bar.3-1-5 return new Integer[] { 3, 1, 5 }
*
* @param effectiveId effective id to check
* @return array of parts, empty array if no parts, null if effectiveId was null
*/
public static int[] getEffectiveIdSuffixParts(String effectiveId) {
if (effectiveId == null)
return null;
final int suffixIndex = effectiveId.indexOf(XFormsConstants.REPEAT_SEPARATOR);
if (suffixIndex != -1) {
final String[] stringResult = StringUtils.split(effectiveId.substring(suffixIndex + 1), XFormsConstants.REPEAT_INDEX_SEPARATOR);
final int[] result = new int[stringResult.length];
for (int i = 0; i < stringResult.length; i++) {
final String currentString = stringResult[i];
result[i] = Integer.parseInt(currentString);
}
return result;
} else {
return EMPTY_INT_ARRAY;
}
}
private static int[] EMPTY_INT_ARRAY = new int[0];
public static String buildEffectiveId(String prefixedId, Object[] iterations) {
if (iterations.length == 0)
return prefixedId;
else
return prefixedId + XFormsConstants.REPEAT_SEPARATOR + StringUtils.join(iterations, XFormsConstants.REPEAT_INDEX_SEPARATOR);
}
/**
* Compute an effective id based on an existing effective id and a static id. E.g.:
*
* foo$bar.1-2 and myStaticId => foo$myStaticId.1-2
*
* @param baseEffectiveId base effective id
* @param staticId static id
* @return effective id
*/
public static String getRelatedEffectiveId(String baseEffectiveId, String staticId) {
final String prefix = getEffectiveIdPrefix(baseEffectiveId);
final String suffix; {
final int suffixIndex = baseEffectiveId.indexOf(XFormsConstants.REPEAT_SEPARATOR);
suffix = (suffixIndex == -1) ? "" : baseEffectiveId.substring(suffixIndex);
}
return prefix + staticId + suffix;
}
/**
* Return the static id associated with the given id, removing suffix and prefix if present.
*
* foo$bar.1-2 => bar
*
* @param anyId id to check
* @return static id, or null if anyId was null
*/
public static String getStaticIdFromId(String anyId) {
return getPrefixedId(getEffectiveIdNoPrefix(anyId));
}
/**
* Append a new string to an effective id.
*
* foo$bar.1-2 and -my-ending => foo$bar-my-ending.1-2
*
* @param effectiveId base effective id
* @param ending new ending
* @return effective id
*/
public static String appendToEffectiveId(String effectiveId, String ending) {
final String prefixedId = getPrefixedId(effectiveId);
return prefixedId + ending + getEffectiveIdSuffixWithSeparator(effectiveId);
}
/**
* Check if an id is a static id, i.e. if it does not contain component/hierarchy separators.
*
* @param id static id to check
* @return true if the id is a static id
*/
public static boolean isStaticId(String id) {
return id != null && id.indexOf(XFormsConstants.COMPONENT_SEPARATOR) == -1 && ! hasEffectiveIdSuffix(id);
}
public static boolean isEffectiveId(String id) {
return id != null && id.indexOf(XFormsConstants.COMPONENT_SEPARATOR) != -1 || hasEffectiveIdSuffix(id);
}
/**
* Whether the id is an absolute id.
*/
public static boolean isAbsoluteId(String id) {
final int l = id.length();
return l >= 3 && id.charAt(0) == XFormsConstants.ABSOLUTE_ID_SEPARATOR && id.charAt(l - 1) == XFormsConstants.ABSOLUTE_ID_SEPARATOR;
}
/**
* Convert an absolute id to an effective id.
*/
public static String absoluteIdToEffectiveId(String absoluteId) {
assert isAbsoluteId(absoluteId);
return absoluteId.substring(1, absoluteId.length() - 1);
}
/**
* Convert an effective id to an absolute id.
*/
public static String effectiveIdToAbsoluteId(String effectiveId) {
return XFormsConstants.ABSOLUTE_ID_SEPARATOR + effectiveId + XFormsConstants.ABSOLUTE_ID_SEPARATOR;
}
public static boolean isTopLevelId(String id) {
// NOTE: Top-level id if static id == prefixed id
return id.equals(XFormsUtils.getStaticIdFromId(id));
}
/**
* Return the id of the enclosing HTML <form> element.
*
* @param containingDocument containing document
* @return id, possibly namespaced
*/
public static String getFormId(XFormsContainingDocument containingDocument) {
return namespaceId(containingDocument, "xforms-form");
}
/**
* Get an element's id.
*
* @param element element to check
* @return id or null
*/
public static String getElementId(Element element) {
return element.attributeValue(XFormsConstants.ID_QNAME);
}
}