/** * 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.processor.transformer; import org.orbeon.dom.Document; import org.orbeon.dom.Element; import org.orbeon.dom.ProcessingInstruction; import org.orbeon.dom.Text; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.processor.*; import org.orbeon.oxf.processor.generator.DOMGenerator; import org.orbeon.oxf.processor.impl.CacheableTransformerOutputImpl; import org.orbeon.oxf.util.PooledXPathExpression; import org.orbeon.oxf.util.XPath; import org.orbeon.oxf.util.XPathCache; import org.orbeon.oxf.xml.EmbeddedDocumentXMLReceiver; import org.orbeon.oxf.xml.NamespaceMapping; import org.orbeon.oxf.xml.TransformerUtils; import org.orbeon.oxf.xml.XMLReceiver; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.oxf.xml.dom4j.LocationData; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.om.NodeInfo; import org.xml.sax.SAXException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; public class XPathProcessor extends ProcessorImpl { private LocationData locationData; public XPathProcessor() { addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG)); addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA)); addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA)); } @Override public void setLocationData(LocationData locationData) { if (locationData != null) this.locationData = locationData; } @Override public ProcessorOutput createOutput(String name) { final ProcessorOutput output = new CacheableTransformerOutputImpl(XPathProcessor.this, name) { public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) { Config config = readCacheInputAsObject(context, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() { public Config read(PipelineContext context, final ProcessorInput input) { final Document config = readInputAsOrbeonDom(context, INPUT_CONFIG); // Get declared namespaces final Map<String, String> namespaces = new HashMap<String, String>(); for (Iterator i = config.getRootElement().elements("namespace").iterator(); i.hasNext();) { Element namespaceElement = (Element) i.next(); namespaces.put(namespaceElement.attributeValue("prefix"), namespaceElement.attributeValue("uri")); } return new Config(new NamespaceMapping(namespaces), config.getRootElement().elements("xpath").get(0).getStringValue()); } }); final DocumentInfo documentInfo = readCacheInputAsTinyTree(context, XPath.GlobalConfiguration(), INPUT_DATA); PooledXPathExpression xpath = null; try { final String baseURI = (locationData == null) ? null : locationData.file(); xpath = XPathCache.getXPathExpression(documentInfo.getConfiguration(), documentInfo, config.getExpression(), config.getNamespaces(), null, org.orbeon.oxf.pipeline.api.FunctionLibrary.instance(), baseURI, locationData); List<Object> results = xpath.evaluateToJavaReturnToPool(); xmlReceiver.startDocument(); // WARNING: Here we break the rule that processors must output valid XML documents, because // we potentially output several root nodes. This works because the XPathProcessor is always // connected to an aggregator, which adds a new root node. for (Iterator i = results.iterator(); i.hasNext();) { Object result = i.next(); if (result == null) continue; streamResult(context, xmlReceiver, result, locationData); } xmlReceiver.endDocument(); } catch (SAXException e) { throw new ValidationException(e, locationData); } } }; addOutput(name, output); return output; } public static void streamResult(PipelineContext context, XMLReceiver xmlReceiver, Object result, LocationData locationData) throws SAXException { String strVal = null; if (result instanceof Element || result instanceof Document) { // If element or document, serialize it to content handler final Element element = result instanceof Element ? (Element) result : ((Document) result).getRootElement(); final String systemId = Dom4jUtils.makeSystemId(element); // TODO: Should probably use Dom4jUtils.createDocumentCopyParentNamespaces() or equivalent to handle namespaces better // -> could maybe simply get the list of namespaces in scope on both sides, and output start/endPrefixMapping() final DOMGenerator domGenerator = new DOMGenerator (element, "xpath result doc", DOMGenerator.ZeroValidity, systemId); final ProcessorOutput domOutput = domGenerator.createOutput(OUTPUT_DATA); domOutput.read(context, new EmbeddedDocumentXMLReceiver(xmlReceiver)); } else if (result instanceof NodeInfo) { final NodeInfo nodeInfo = (NodeInfo) result; TransformerUtils.writeTinyTree(nodeInfo, new EmbeddedDocumentXMLReceiver(xmlReceiver)); } else if (result instanceof ProcessingInstruction) { ProcessingInstruction processingInstruction = (ProcessingInstruction) result; xmlReceiver.processingInstruction(processingInstruction.getTarget(), processingInstruction.getText()); } else if (result instanceof Text) { strVal = ((Text) result).getText(); } else if (result instanceof String) { strVal = (String) result; } else if (result instanceof Long) { strVal = Long.toString((Long) result); } else if (result instanceof Double) { final double d = ((Double) result).doubleValue(); strVal = Double.toString(d); } else if (result instanceof Boolean) { strVal = (result.toString()); } else if (result instanceof StringBuilder) { strVal = result.toString(); } else { String message = "Unsupported type returned by XPath expression: " + (result == null ? "null" : result.getClass().getName()); throw new ValidationException(message, locationData); } // Send string representation of simple type to content handler if (strVal != null) { final char[] ch = strVal.toCharArray(); final int len = strVal.length(); xmlReceiver.characters( ch, 0, len ); } } protected static class Config { private final NamespaceMapping namespaces; private final String expression; public Config(NamespaceMapping namespaces, String expression) { this.namespaces = namespaces; this.expression = expression; } public String getExpression() { return expression; } public NamespaceMapping getNamespaces() { return namespaces; } } }