/** * 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.pipeline.foreach; import org.orbeon.dom.Document; import org.orbeon.dom.Element; import org.orbeon.dom.Node; import org.orbeon.dom.Node$; import org.orbeon.oxf.cache.OutputCacheKey; import org.orbeon.oxf.common.OXFException; 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.pipeline.PipelineProcessor; import org.orbeon.oxf.processor.pipeline.TeeProcessor; import org.orbeon.oxf.processor.pipeline.ast.*; import org.orbeon.oxf.util.PooledXPathExpression; import org.orbeon.oxf.util.XPathCache; import org.orbeon.oxf.xml.EmbeddedDocumentXMLReceiver; import org.orbeon.oxf.xml.NamespaceMapping; import org.orbeon.oxf.xml.XMLReceiver; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.dom.saxon.DocumentWrapper; import org.orbeon.saxon.om.DocumentInfo; import org.orbeon.saxon.trans.XPathException; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import java.util.*; public class ConcreteForEachProcessor extends ProcessorImpl { private final Processor forEachBlockProcessor; private final ProcessorOutput iterationOutput; private final String select; private final NamespaceMapping namespaceContext; private String rootLocalName; private String rootQName; private String rootNamespaceURI; public ConcreteForEachProcessor(ASTForEach forEachAST, Object validity) { final String[] refsWithNoId = getRefsWithNoId(forEachAST); final String idOrRef = forEachAST.getId() != null ? forEachAST.getId() : forEachAST.getRef(); // Create pipeline to represent the nested pipeline block within p:for-each { final ASTPipeline astPipeline = new ASTPipeline(); astPipeline.setValidity(validity); astPipeline.getStatements().addAll(forEachAST.getStatements()); astPipeline.setNode(forEachAST.getNode()); for (int i = 0; i < refsWithNoId.length; i++) { astPipeline.addParam(new ASTParam(ASTParam.INPUT, refsWithNoId[i])); if (!refsWithNoId[i].equals(AbstractForEachProcessor.FOR_EACH_CURRENT_INPUT)) addInputInfo(new ProcessorInputOutputInfo(refsWithNoId[i])); } if (idOrRef != null) { astPipeline.addParam(new ASTParam(ASTParam.OUTPUT, idOrRef)); addOutputInfo(new ProcessorInputOutputInfo(idOrRef)); } if (logger.isDebugEnabled()) { final ASTDocumentHandler astDocumentHandler = new ASTDocumentHandler(); astPipeline.walk(astDocumentHandler); logger.debug("Iteration pipeline:\n" + Dom4jUtils.domToString(astDocumentHandler.getDocument())); } forEachBlockProcessor = new PipelineProcessor(astPipeline); } // Connect nested pipeline block inputs to inputs of p:for-each processor for (int i = 0; i < refsWithNoId.length; i++) { if (!refsWithNoId[i].equals(AbstractForEachProcessor.FOR_EACH_CURRENT_INPUT)) { final ProcessorInput pipelineInput = forEachBlockProcessor.createInput(refsWithNoId[i]); pipelineInput.setOutput(new ForwardingProcessorOutput(refsWithNoId[i])); } } // Connect special "$current" input which produces the document selected by the iteration final ProcessorInput iterationInput = forEachBlockProcessor.createInput(AbstractForEachProcessor.FOR_EACH_CURRENT_INPUT); final ProcessorOutput currentOutput = new IterationProcessorOutput(AbstractForEachProcessor.FOR_EACH_CURRENT_INPUT); currentOutput.setInput(iterationInput); iterationInput.setOutput(currentOutput); // Create output for the iteration iterationOutput = forEachBlockProcessor.createOutput(idOrRef); select = forEachAST.getSelect(); namespaceContext = new NamespaceMapping(Dom4jUtils.getNamespaceContextNoDefault((Element) forEachAST.getNode())); if (forEachAST.getRoot() != null) { rootQName = forEachAST.getRoot(); int columnPosition = rootQName.indexOf(':'); if (columnPosition == -1) { // No prefix rootLocalName = rootQName; rootNamespaceURI = ""; } else { // Extract prefix, find namespace URI final String prefix = rootQName.substring(0, columnPosition); rootNamespaceURI = namespaceContext.mapping.get(prefix); if (rootNamespaceURI == null) throw new ValidationException("Prefix '" + prefix + "' used in root attribute is undefined", forEachAST.getLocationData()); rootLocalName = rootQName.substring(columnPosition + 1); } } } @Override public ProcessorOutput createOutput(final String name) { final ProcessorOutput output = new ProcessorOutputImpl(ConcreteForEachProcessor.this, name) { public void readImpl(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { try { final State state = (State) getState(pipelineContext); // Open document xmlReceiver.startDocument(); xmlReceiver.startElement(rootNamespaceURI, rootLocalName, rootQName, new AttributesImpl()); // Read n times from iterationOutput PooledXPathExpression expression = null; int iterationCount = 0; try { expression = createExpression(pipelineContext); for (Iterator i = new ElementIterator(expression); i.hasNext(); iterationCount++) { final Element currentElement = (Element) i.next(); // Create DOMGenerator final String systemId = Dom4jUtils.makeSystemId(currentElement); final DOMGenerator domGenerator = new DOMGenerator (currentElement, "for each input", DOMGenerator.ZeroValidity, systemId); domGenerator.createOutput(OUTPUT_DATA); state.domGenerator = domGenerator; // Run iteration forEachBlockProcessor.reset(pipelineContext); iterationOutput.read(pipelineContext, new EmbeddedDocumentXMLReceiver(xmlReceiver)); } } catch (XPathException e) { throw new OXFException(e); } finally { // Clear state to allow gc as the state might be referenced for a while if (state != null) state.domGenerator = null; // Return expression if (expression != null) expression.returnToPool(); } // Notify input Tee processors that we are done commitInputs(pipelineContext, iterationCount); // Close document xmlReceiver.endElement(rootNamespaceURI, rootLocalName, rootQName); xmlReceiver.endDocument(); } catch (SAXException e) { throw new OXFException(e); } } /** * For each is not cacheable. We used to have code here that combines the keys for * the outputs of the different executions of the for-each. The problem is when we * have a block in the for-each that has an output and a serializer. In that case, * getting the output key runs the serializer. * * So if we get the key here, we'll run the serializer n times, which is * unexpected. Maybe an an API that combines reading the output and reading the * key/validity would solve this problem. */ @Override public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) { return null; } /** * For each is not cacheable. See comment in getKeyImpl(). */ @Override protected Object getValidityImpl(PipelineContext pipelineContext) { return null; } }; addOutput(name, output); return output; } private void commitInputs(PipelineContext pipelineContext, int iterationCount) { for (Iterator<Map.Entry<String,List<ProcessorInput>>> i = getConnectedInputs().entrySet().iterator(); i.hasNext();) { final Map.Entry<String,List<ProcessorInput>> entry = i.next(); final List<ProcessorInput> inputs = entry.getValue(); final ProcessorInput input = inputs.get(0);// NOTE: We don't really support multiple inputs with the same name. if (!AbstractForEachProcessor.FOR_EACH_DATA_INPUT.equals(input.getName())) {// ignore $data final ProcessorOutput output = input.getOutput(); if (output instanceof TeeProcessor.TeeProcessorOutputImpl) { final TeeProcessor.TeeProcessorOutputImpl teeOutput = (TeeProcessor.TeeProcessorOutputImpl) output; teeOutput.doneReading(pipelineContext); } } } } private class ElementIterator implements Iterator { private final Iterator iterator; public ElementIterator(PooledXPathExpression expression) throws XPathException { this.iterator = expression.iterateReturnJavaObjects(); } public boolean hasNext() { return iterator.hasNext(); } public Object next() { final Object nextObject = iterator.next(); if (nextObject instanceof Node) { final Node nextNode = (Node) nextObject; if (! (nextNode instanceof Element)) throw new OXFException("Select expression '" + select + "' did not return a sequence of elements. One node was a '" + Node$.MODULE$.nodeTypeName(nextNode) + "'"); } else { throw new OXFException("Select expression '" + select + "' did not return a sequence of elements. One item was a '" + nextObject.getClass().getName() + "'"); } return nextObject; } public void remove() { throw new UnsupportedOperationException(); } } private PooledXPathExpression createExpression(PipelineContext pipelineContext) { // Read special "$data" input final Document dataInput = readInputAsOrbeonDom(pipelineContext, getInputByName(AbstractForEachProcessor.FOR_EACH_DATA_INPUT)); final DocumentInfo document = new DocumentWrapper(dataInput, null, org.orbeon.oxf.util.XPath.GlobalConfiguration()); return XPathCache.getXPathExpression( document.getConfiguration(), document, select, namespaceContext, getLocationData()); } @Override public void start(PipelineContext pipelineContext) { final State state = (State) getState(pipelineContext); // Read n times from iterationOutput PooledXPathExpression expression = null; int iterationCount = 0; try { expression = createExpression(pipelineContext); for (Iterator i = new ElementIterator(expression); i.hasNext(); iterationCount++) { final Element currentElement = (Element) i.next(); // Create DOMGenerator final String systemId = Dom4jUtils.makeSystemId(currentElement); final DOMGenerator domGenerator = new DOMGenerator (currentElement, "for each input", DOMGenerator.ZeroValidity, systemId); domGenerator.createOutput(OUTPUT_DATA); state.domGenerator = domGenerator; // Run iteration forEachBlockProcessor.reset(pipelineContext); forEachBlockProcessor.start(pipelineContext); } } catch (XPathException e) { throw new OXFException(e); } finally { // Clear state to allow gc as the state might be referenced for a while if (state != null) state.domGenerator = null; // Return expression if (expression != null) expression.returnToPool(); } // Notify input Tee processors that we are done commitInputs(pipelineContext, iterationCount); } /** * Determine all <p:input ref="..."> with no <p:output id="...">. * Those are the inputs of this processor. */ private String[] getRefsWithNoId(ASTForEach forEachAST) { // All the ids references from <p:input href="..."> final Set inputRefs = new HashSet(); // All the ids in <p:output id="..."> final Set outputIds = new HashSet(); // Init the 2 sets above for (Iterator<ASTStatement> i = forEachAST.getStatements().iterator(); i.hasNext();) { final ASTStatement astStatement = i.next(); final IdInfo idInfo = astStatement.getIdInfo(); inputRefs.addAll(idInfo.getInputRefs()); outputIds.addAll(idInfo.getOutputIds()); } final Set refsWithNoId = new HashSet(inputRefs); refsWithNoId.removeAll(outputIds); return (String[]) refsWithNoId.toArray(new String[refsWithNoId.size()]); } /** * Special processor output which delegates to the p:for-each input */ private class ForwardingProcessorOutput extends ProcessorOutputImpl { public ForwardingProcessorOutput(String name) { super(ConcreteForEachProcessor.this, name); } protected void readImpl(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { // Delegate to the p:for-each input ConcreteForEachProcessor.this.readInputAsSAX(pipelineContext, getName(), xmlReceiver); } @Override public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) { // NOTE: this means that the execution of the pipeline block is not cacheable. Should improve? return null; } @Override protected Object getValidityImpl(PipelineContext pipelineContext) { // NOTE: this means that the execution of the pipeline block is not cacheable. Should improve? return null; } } /** * Reads from the DOM generator stored in state. */ private class IterationProcessorOutput extends ProcessorOutputImpl { public IterationProcessorOutput(String name) { super(ConcreteForEachProcessor.this, name); } protected void readImpl(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { final State state = (State) getState(pipelineContext); state.domGenerator.getOutputByName(OUTPUT_DATA).read(pipelineContext, xmlReceiver); } @Override public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) { final State state = (State) getState(pipelineContext); return state.domGenerator.getOutputByName(OUTPUT_DATA).getKey(pipelineContext); } @Override protected Object getValidityImpl(PipelineContext pipelineContext) { final State state = (State) getState(pipelineContext); return state.domGenerator.getOutputByName(OUTPUT_DATA).getValidity(pipelineContext); } } /** * Runtime state information for p:for-each. */ private static class State { DOMGenerator domGenerator; } @Override public void reset(PipelineContext pipelineContext) { setState(pipelineContext, new State()); } }