/** * 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.transformer.xupdate.statement; import org.orbeon.dom4j.Attribute; import org.orbeon.dom4j.*; import org.orbeon.dom4j.XPath; import org.orbeon.jaxen.*; import org.orbeon.jaxen.dom4j.DocumentNavigator; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.transformer.xupdate.Closure; import org.orbeon.oxf.transformer.xupdate.DocumentContext; import org.orbeon.oxf.transformer.xupdate.Statement; import org.orbeon.oxf.transformer.xupdate.VariableContextImpl; import org.orbeon.oxf.transformer.xupdate.dom4j.Dom4jUtils; import org.orbeon.oxf.transformer.xupdate.dom4j.LocationSAXContentHandler; import org.orbeon.oxf.xml.dom4j.LocationData; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; import javax.xml.transform.Source; import javax.xml.transform.URIResolver; import javax.xml.transform.sax.SAXSource; import java.util.*; public class Utils { /** * Evaluate a sequence a statements, and insert the result of the * evaluation the given parent node at the given position. */ public static void insert(LocationData locationData, Node parent, int position, Object toInsert) { List nodesToInsert = xpathObjectToDOM4JList(locationData, toInsert); if (parent instanceof Element) Collections.reverse(nodesToInsert); for (Iterator j = nodesToInsert.iterator(); j.hasNext();) { Object object = j.next(); Node node = object instanceof String || object instanceof Number ? Dom4jUtils.createText(object.toString()) : (Node) ((Node) object).clone(); if (parent instanceof Element) { Element element = (Element) parent; if (node instanceof Attribute) { element.attributes().add(node); } else { element.content().add(position, node); } } else if (parent instanceof Attribute) { Attribute attribute = (Attribute) parent; attribute.setValue(attribute.getValue() + node.getText()); } else if (parent instanceof Document) { // Update a document element final Document document = (Document) parent; if (node instanceof Element) { if (document.getRootElement() != null) throw new ValidationException("Document already has a root element", locationData); document.setRootElement((Element) node); } else if (node instanceof ProcessingInstruction) { document.add(node); } else { throw new ValidationException("Only an element or processing instruction can be at the root of a document", locationData); } } else { throw new ValidationException("Cannot insert into a node of type '" + parent.getClass() + "'", locationData); } } } /** * Evaluates an XPath expression */ public static Object evaluate(final URIResolver uriResolver, Object context, final VariableContextImpl variableContext, final DocumentContext documentContext, final LocationData locationData, String select, NamespaceContext namespaceContext) { FunctionContext functionContext = new FunctionContext() { public org.orbeon.jaxen.Function getFunction(final String namespaceURI, final String prefix, final String localName) { // Override document() and doc() if (/*namespaceURI == null &&*/ ("document".equals(localName) || "doc".equals(localName))) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { try { String url = (String) Dom4jUtils.createXPath("string(.)").evaluate(args.get(0)); Document result = documentContext.getDocument(url); if (result == null) { Source source = uriResolver.resolve(url, locationData.file()); if (! (source instanceof SAXSource)) throw new ValidationException("Unsupported source type", locationData); XMLReader xmlReader = ((SAXSource) source).getXMLReader(); LocationSAXContentHandler contentHandler = new LocationSAXContentHandler(); xmlReader.setContentHandler(contentHandler); xmlReader.parse(new InputSource()); result = contentHandler.getDocument(); documentContext.addDocument(url, result); } return result; } catch (Exception e) { throw new ValidationException(e, locationData); } } }; } else if (/*namespaceURI == null &&*/ "get-namespace-uri-for-prefix".equals(localName)) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { String prefix = (String) Dom4jUtils.createXPath("string(.)").evaluate(args.get(0)); Element element = null; if (args.get(1) instanceof List) { List list = (List) args.get(1); if (list.size() == 1) element = (Element) list.get(0); } else if (args.get(1) instanceof Element) { element = (Element) args.get(1); } if (element == null) throw new ValidationException("An element is expected as the second argument " + "in get-namespace-uri-for-prefix()", locationData); return element.getNamespaceForPrefix(prefix); } }; } else if (/*namespaceURI == null &&*/ "distinct-values".equals(localName)) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { List originalList = args.get(0) instanceof List ? (List) args.get(0) : Collections.singletonList(args.get(0)); List resultList = new ArrayList(); XPath stringXPath = Dom4jUtils.createXPath("string(.)"); for (Iterator i = originalList.iterator(); i.hasNext();) { Object item = (Object) i.next(); String itemString = (String) stringXPath.evaluate(item); if (!resultList.contains(itemString)) resultList.add(itemString); } return resultList; } }; } else if (/*namespaceURI == null &&*/ "evaluate".equals(localName)) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { try { if (args.size() != 3) { try { return XPathFunctionContext.getInstance().getFunction(namespaceURI, prefix, localName); } catch (UnresolvableException e) { throw new ValidationException(e, locationData); } } else { String xpathString = (String) Dom4jUtils.createXPath("string(.)").evaluate(args.get(0)); XPath xpath = Dom4jUtils.createXPath(xpathString); Map namespaceURIs = new HashMap(); List namespaces = (List) args.get(1); for (Iterator i = namespaces.iterator(); i.hasNext();) { org.orbeon.dom4j.Namespace namespace = (org.orbeon.dom4j.Namespace) i.next(); namespaceURIs.put(namespace.getPrefix(), namespace.getURI()); } xpath.setNamespaceURIs(namespaceURIs); return xpath.evaluate(args.get(2)); } } catch (InvalidXPathException e) { throw new ValidationException(e, locationData); } } }; } else if (/*namespaceURI == null &&*/ "tokenize".equals(localName)) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { try { String input = (String) Dom4jUtils.createXPath("string(.)").evaluate(args.get(0)); String pattern = (String) Dom4jUtils.createXPath("string(.)").evaluate(args.get(1)); List result = new ArrayList(); while (input.length() != 0) { int position = input.indexOf(pattern); if (position != -1) { result.add(input.substring(0, position)); input = input.substring(position + 1); } else { result.add(input); input = ""; } } return result; } catch (InvalidXPathException e) { throw new ValidationException(e, locationData); } } }; } else if (/*namespaceURI == null &&*/ "string-join".equals(localName)) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { try { List strings = (List) args.get(0); String pattern = (String) Dom4jUtils.createXPath("string(.)").evaluate(args.get(1)); StringBuilder result = new StringBuilder(); boolean isFirst = true; for (Iterator i = strings.iterator(); i.hasNext();) { if (! isFirst) result.append(pattern); else isFirst = false; String item = (String) (String) Dom4jUtils.createXPath("string(.)").evaluate(i.next()); result.append(item); } return result.toString(); } catch (InvalidXPathException e) { throw new ValidationException(e, locationData); } } }; } else if (/*namespaceURI == null &&*/ "reverse".equals(localName)) { return new org.orbeon.jaxen.Function() { public Object call(Context jaxenContext, List args) { try { List result = new ArrayList((List) args.get(0)); Collections.reverse(result); return result; } catch (InvalidXPathException e) { throw new ValidationException(e, locationData); } } }; } else { try { // Go through standard XPath functions return XPathFunctionContext.getInstance().getFunction(namespaceURI, prefix, localName); } catch (UnresolvableException e) { // User-defined function try { final Closure closure = findClosure(variableContext.getVariableValue (namespaceURI, prefix, localName)); if (closure == null) throw new ValidationException("'" + qualifiedName(prefix, localName) + "' is not a function", locationData); return new org.orbeon.jaxen.Function() { public Object call(Context context, List args) { return closure.execute(args); } }; } catch (UnresolvableException e2) { throw new ValidationException("Cannot invoke function '" + qualifiedName(prefix, localName) + "', no such function", locationData); } } } } private Closure findClosure(Object xpathObject) { if (xpathObject instanceof Closure) { return (Closure) xpathObject; } else if (xpathObject instanceof List) { for (Iterator i = ((List) xpathObject).iterator(); i.hasNext();) { Closure closure = findClosure(i.next()); if (closure != null) return closure; } return null; } else { return null; } } }; try { // Create XPath XPath xpath = Dom4jUtils.createXPath(select); // Set variable, namespace, and function context if (context instanceof Context) { // Create a new context, as Jaxen may modify the current node in the context (is this a bug?) Context currentContext = (Context) context; Context newContext = new Context(new ContextSupport (namespaceContext, functionContext, variableContext, DocumentNavigator.getInstance())); newContext.setNodeSet(currentContext.getNodeSet()); newContext.setSize(currentContext.getSize()); newContext.setPosition(currentContext.getPosition()); context = newContext; } else { xpath.setVariableContext(variableContext); xpath.setNamespaceContext(namespaceContext); xpath.setFunctionContext(functionContext); } // Execute XPath return xpath.evaluate(context); } catch (Exception e) { throw new ValidationException(e, locationData); } } public static List evaluateToList(URIResolver uriResolver, Object context, VariableContextImpl variableContext, LocationData locationData, String select, NamespaceContext namespaceContext, DocumentContext documentContext) { Object selected = Utils.evaluate(uriResolver, context, variableContext, documentContext, locationData, select, namespaceContext); return xpathObjectToDOM4JList(locationData, selected); } public static List xpathObjectToDOM4JList(LocationData locationData, Object selected) { if (selected == null) { return Collections.EMPTY_LIST; } else if (selected instanceof String || selected instanceof Number) { org.orbeon.dom4j.Text textNode = Dom4jUtils.createText(selected.toString()); return Arrays.asList(new org.orbeon.dom4j.Text[] {textNode}); } else if (selected instanceof Node) { return Arrays.asList(new Node[]{(Node) selected}); } else if (selected instanceof List) { return (List) selected; } else if (selected instanceof Closure) { return Arrays.asList(new Closure[] {(Closure) selected}); } else { throw new ValidationException("Unsupported type: " + selected.getClass().getName(), locationData); } } public static Object execute(URIResolver uriResolver, Object context, VariableContextImpl variableContext, DocumentContext documentContext, Statement[] statements) { List result = new ArrayList(); VariableContextImpl currentVariableContext = variableContext; for (int i = 0; i < statements.length; i++) { if (statements[i] instanceof Variable) { Variable variable = (Variable) statements[i]; currentVariableContext = new VariableContextImpl(currentVariableContext, variable.getName(), variable.execute(uriResolver, context, currentVariableContext, documentContext)); } else if (statements[i] instanceof Function) { Function function = (Function) statements[i]; Closure closure = function.getClosure(uriResolver, context); currentVariableContext = new VariableContextImpl(currentVariableContext, function.getName(), closure); closure.setVariableContext(currentVariableContext); } else { Object statementResult = statements[i].execute(uriResolver, context, currentVariableContext, documentContext); if (statementResult instanceof List) { result.addAll((List) statementResult); } else { result.add(statementResult); } } } return result.size() == 1 ? result.get(0) : result; } public static String xpathObjectToString(Object xpathObject) { List list = xpathObject instanceof List ? (List) xpathObject : Collections.singletonList(xpathObject); StringBuilder buffer = new StringBuilder(); for (Iterator i = list.iterator(); i.hasNext();) { Object object = i.next(); buffer.append(object instanceof Node ? Dom4jUtils.nodeToString((Node) object) : object.toString()); } return buffer.toString(); } public static String qualifiedName(String prefix, String localName) { return ("".equals(prefix) ? "" : prefix + ":") + localName; } public static Element getInsertPivot(LocationData locationData, String select, NamespaceContext namespaceContext, URIResolver uriResolver, Object context, VariableContextImpl variableContext, DocumentContext documentContext) { Object insertPivotObject = Utils.evaluate(uriResolver, context, variableContext, documentContext, locationData, select, namespaceContext); if (!(insertPivotObject instanceof Element)) throw new ValidationException("Select expression must return an element", locationData); return (Element) insertPivotObject; } }