/** * 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; import org.orbeon.dom.Document; import org.orbeon.dom.DocumentFactory; import org.orbeon.dom.Element; import org.orbeon.dom.Node; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.processor.Processor; import org.orbeon.oxf.processor.ProcessorImpl; import org.orbeon.oxf.processor.ProcessorInput; import org.orbeon.oxf.processor.ProcessorOutput; import org.orbeon.oxf.processor.generator.DOMGenerator; import org.orbeon.oxf.processor.generator.URLGenerator; import org.orbeon.oxf.processor.pipeline.ast.*; import org.orbeon.oxf.processor.pipeline.foreach.AbstractForEachProcessor; import org.orbeon.oxf.processor.transformer.XPathProcessor; import org.orbeon.oxf.resources.URLFactory; import org.orbeon.oxf.util.PipelineUtils; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.oxf.xml.dom4j.LocationData; import java.net.URL; import java.util.*; /** * This class encapsulates the data structures visible from a pipeline. */ public class PipelineBlock { // Maps: (String outputId) -> (ProcessorOutput) private Map idToOutputMap = new HashMap(); // Maps: (String paramOutputId) -> (ProcessorInput) private Map idToInputMap = new HashMap(); // Maps: (String outputId) -> (TeeProcessor) private Map outputIdToTeeProcessor = new HashMap(); // Set of (String paramOutputId) private Set inputIdAlreadyConnected = new HashSet(); // Set of processors we create in the class for connection purposes private Set createdProcessors = new HashSet(); public void declareOutput(Node node, String id, ProcessorOutput output) { if (idToOutputMap.containsKey(id)) { LocationData locationData = node == null ? null : (LocationData) ((Element) node).getData(); throw new ValidationException("Output id \"" + id + "\" is already declared in pipeline", locationData); } idToOutputMap.put(id, output); } public ProcessorInput connectProcessorToHref(Node node, Processor processor, String inputName, ASTHref href) { return connectProcessorToHref(node, processor, inputName, href, false); } public ProcessorInput connectProcessorToHref(Node node, Processor processor, String inputName, ASTHref href, boolean isFromInnerforEach) { final LocationData locationData = node == null ? null : (LocationData) ((Element) node).getData(); final ProcessorInput processorInput = processor.createInput(inputName); if (href instanceof ASTHrefId) { final String referencedId = ((ASTHrefId) href).getId(); // Nice messsage for current() if (referencedId.equals(AbstractForEachProcessor.FOR_EACH_CURRENT_INPUT) && !idToOutputMap.containsKey(AbstractForEachProcessor.FOR_EACH_CURRENT_INPUT)) throw new ValidationException("Function current() can only be used in a for-each block", locationData); // Reference to previously defined id if (!idToOutputMap.containsKey(referencedId)) throw new ValidationException("Reference to undeclared output id \"" + referencedId + "\"", locationData); final ProcessorOutput referencedOutput = (ProcessorOutput) idToOutputMap.get(referencedId); if (!isFromInnerforEach && referencedOutput.getInput() == null) { // Output is virgin: just connect it to this processor processorInput.setOutput(referencedOutput); referencedOutput.setInput(processorInput); } else if (outputIdToTeeProcessor.containsKey(referencedId)) { // ASTInput already shared: just add ourselves to the tee final TeeProcessor tee = (TeeProcessor) outputIdToTeeProcessor.get(referencedId); final ProcessorOutput teeOutput = tee.createOutput(ProcessorImpl.OUTPUT_DATA, isFromInnerforEach); teeOutput.setInput(processorInput); processorInput.setOutput(teeOutput); } else { // Force menage a trois by introducing a tee final TeeProcessor tee = new TeeProcessor(locationData); createdProcessors.add(tee); outputIdToTeeProcessor.put(referencedId, tee); if (referencedOutput.getInput() != null) { // can be null if isFromInnerforEach == true // Reconnect the "other guy" to the tee output final ProcessorOutput firstTeeOutput = tee.createOutput(ProcessorImpl.OUTPUT_DATA, false); final ProcessorInput otherGuyInput = referencedOutput.getInput(); firstTeeOutput.setInput(otherGuyInput); otherGuyInput.setOutput(firstTeeOutput); } // Connect tee to processor final ProcessorOutput secondTeeOutput = tee.createOutput(ProcessorImpl.OUTPUT_DATA, isFromInnerforEach); secondTeeOutput.setInput(processorInput); processorInput.setOutput(secondTeeOutput); // Connect tee input final ProcessorInput teeInput = tee.createInput(ProcessorImpl.INPUT_DATA); teeInput.setOutput(referencedOutput); referencedOutput.setInput(teeInput); } } else if (href instanceof ASTHrefAggregate) { ASTHrefAggregate hrefAggregate = (ASTHrefAggregate) href; // Connect aggregator to config final Processor aggregator = new AggregatorProcessor(); final Document aggregatorConfig = DocumentFactory.createDocument(); final Element configElement = aggregatorConfig.addElement("config"); configElement.addElement("root").addText(hrefAggregate.getRoot()); addNamespaces(configElement, node, false); final String sid = locationData == null ? DOMGenerator.DefaultContext : locationData.file(); final DOMGenerator configGenerator = new DOMGenerator ( aggregatorConfig, "aggregate config", DOMGenerator.ZeroValidity, sid ); PipelineUtils.connect(configGenerator, ProcessorImpl.OUTPUT_DATA, aggregator, ProcessorImpl.INPUT_CONFIG); // Connect data input to aggregator for (Iterator i = hrefAggregate.getHrefs().iterator(); i.hasNext();) { final ASTHref id = (ASTHref) i.next(); connectProcessorToHref(node, aggregator, ProcessorImpl.INPUT_DATA, id); } // Connect aggregator output to current processor final ProcessorOutput aggregatorOutput = aggregator.createOutput(ProcessorImpl.OUTPUT_DATA); aggregatorOutput.setInput(processorInput); processorInput.setOutput(aggregatorOutput); } else if (href instanceof ASTHrefURL) { // Get the docbase url from the location data if available // and concatenate it with the current href final URL url = URLFactory.createURL(locationData != null && locationData.file() != null ? locationData.file() : null, ((ASTHrefURL) href).getURL()); // This is interpreted as a URL, use URL Generator final Processor urlGenerator = new URLGenerator(url.toExternalForm()); final ProcessorOutput referencedOutput = urlGenerator.createOutput(ProcessorImpl.OUTPUT_DATA); referencedOutput.setInput(processorInput); processorInput.setOutput(referencedOutput); } else if (href instanceof ASTHrefXPointer) { final ASTHrefXPointer hrefXPointer = (ASTHrefXPointer) href; // Create config for XPath processor final Document xpathConfig = DocumentFactory.createDocument(); final Element configElement = xpathConfig.addElement("config"); configElement.addElement("xpath").addText(hrefXPointer.getXpath()); addNamespaces(configElement, node, true); // Connect XPath processor to config final Processor xpathProcessor = new XPathProcessor(); xpathProcessor.setLocationData(locationData); // TODO FIXME: I suspect locationData will always be null here because node is null! final String sid = locationData == null ? DOMGenerator.DefaultContext : locationData.file(); final DOMGenerator configGenerator = new DOMGenerator ( xpathConfig, "xpath config", DOMGenerator.ZeroValidity, sid ); PipelineUtils.connect(configGenerator, ProcessorImpl.OUTPUT_DATA, xpathProcessor, ProcessorImpl.INPUT_CONFIG); // Connect data input to XPath processor connectProcessorToHref(node, xpathProcessor, ProcessorImpl.INPUT_DATA, hrefXPointer.getHref()); // Connect XPath processor output to current processor final ProcessorOutput xpathOutput = xpathProcessor.createOutput(ProcessorImpl.OUTPUT_DATA); xpathOutput.setInput(processorInput); processorInput.setOutput(xpathOutput); } else { throw new ValidationException("Unsupported href type", locationData); } return processorInput; } private void addNamespaces(Element configElement, Node node, boolean addPipelineNamespace) { if (node != null) { final Map<String, String> namespaces = Dom4jUtils.getNamespaceContextNoDefault((Element) node); for (final Map.Entry<String, String> entry: namespaces.entrySet()) { final String prefix = entry.getKey(); final String uri = entry.getValue(); final Element namespaceElement = configElement.addElement("namespace"); namespaceElement.addAttribute("prefix", prefix); namespaceElement.addAttribute("uri", uri); } } else if (addPipelineNamespace) { // HACK: we do this just to facilitate the job of the PFC final Element namespaceElement = configElement.addElement("namespace"); namespaceElement.addAttribute("prefix", "p"); namespaceElement.addAttribute("uri", PipelineProcessor.PIPELINE_NAMESPACE_URI); } } public void declareBottomInput(Node node, String id, ProcessorInput input) { if (idToInputMap.containsKey(id)) { final LocationData locationData = (LocationData) ((Element) node).getData(); throw new ValidationException("There can be only one output parameter with id \"" + id + "\" in a pipeline", locationData); } idToInputMap.put(id, input); } public boolean isBottomInputConnected(String id) { final ProcessorInput bottomInput = (ProcessorInput) idToInputMap.get(id); return bottomInput.getOutput() != null; } public ProcessorOutput connectProcessorToBottomInput(Node node, String outputName, String referencedId, ProcessorOutput processorOutput) { if (!idToInputMap.containsKey(referencedId)) { final LocationData locationData = node == null ? null : (LocationData) ((Element) node).getData(); throw new ValidationException("Reference to undeclared output parameter id \"" + referencedId + "\"", locationData); } if (inputIdAlreadyConnected.contains(referencedId)) { final LocationData locationData = node == null ? null : (LocationData) ((Element) node).getData(); throw new ValidationException("Other processor output is already connected to output parameter id \"" + referencedId + "\"", locationData); } final ProcessorInput bottomInput = (ProcessorInput) idToInputMap.get(referencedId); bottomInput.setOutput(processorOutput); processorOutput.setInput(bottomInput); return processorOutput; } public Set getCreatedProcessors() { return createdProcessors; } }