/** * 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.choose; import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.orbeon.oxf.cache.OutputCacheKey; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.util.XPath; import org.orbeon.oxf.xml.XMLReceiver; import org.orbeon.oxf.processor.*; import org.orbeon.oxf.util.LoggerFactory; import org.orbeon.oxf.util.PooledXPathExpression; import org.orbeon.oxf.util.XPathCache; import org.orbeon.oxf.xml.NamespaceMapping; import org.orbeon.oxf.xml.dom4j.LocationData; import org.orbeon.saxon.Configuration; import org.orbeon.saxon.om.DocumentInfo; import java.util.*; public class ConcreteChooseProcessor extends ProcessorImpl { public static Logger logger = LoggerFactory.createLogger(ConcreteChooseProcessor.class); // Created when constructed private LocationData locationData; private List branchConditions; private List<NamespaceMapping> branchNamespaces; private List<Processor> branchProcessors; private Set outputsById; private Set outputsByParamRef; private List<Map<String, ProcessorInput>> branchInputs = new ArrayList<Map<String, ProcessorInput>>(); // List [Map: (String inputName) -> (ProcessorInput)] private List<Map<Object, ProcessorOutput>> branchOutputs = new ArrayList<Map<Object, ProcessorOutput>>(); // List [Map: (String outputName) -> (ProcessorOutput)] /** * @param branchConditions List of Strings: XPath expression for each branch * (except the optimal last <otherwise>) * @param branchNamespaces List of NamespaceContext objects: namespaces declared in * the context of the given XPath expression * @param branchProcessors List of Processor objects: one for each branch * @param inputs Set of Strings: all the ids possibly referenced by * a processor in any branch * @param outputsById Set of Strings: outputs of the choose referenced * by and other processor * @param outputsByParamRef Set of Strings: outputs of the choose referencing * pipeline outputs */ public ConcreteChooseProcessor(String id, LocationData locationData, List branchConditions, List<NamespaceMapping> branchNamespaces, List<Processor> branchProcessors, Set inputs, Set outputsById, Set outputsByParamRef) { setId(id); this.locationData = locationData; this.branchConditions = branchConditions; this.branchNamespaces = branchNamespaces; this.branchProcessors = branchProcessors; this.outputsById = outputsById; this.outputsByParamRef = outputsByParamRef; // Add inputs addInputInfo(new ProcessorInputOutputInfo(AbstractChooseProcessor.CHOOSE_DATA_INPUT)); for (Iterator i = inputs.iterator(); i.hasNext();) { String name = (String) i.next(); addInputInfo(new ProcessorInputOutputInfo(name)); } // Add outputs for (Iterator i = CollectionUtils.union(outputsById, outputsByParamRef).iterator(); i.hasNext();) { String name = (String) i.next(); addOutputInfo(new ProcessorInputOutputInfo(name)); } for (Iterator i = branchProcessors.iterator(); i.hasNext();) { Processor processor = (Processor) i.next(); // Create ProcessorInput for each branch Map<String, ProcessorInput> currentBranchInputs = new HashMap<String, ProcessorInput>(); branchInputs.add(currentBranchInputs); for (Iterator j = inputs.iterator(); j.hasNext();) { String inputName = (String) j.next(); currentBranchInputs.put(inputName, processor.createInput(inputName)); } // Create ProcessorOutput for each branch Map<Object, ProcessorOutput> currentBranchOutputs = new HashMap<Object, ProcessorOutput>(); branchOutputs.add(currentBranchOutputs); for (Iterator j = CollectionUtils.union(outputsById, outputsByParamRef).iterator(); j.hasNext();) { String outputName = (String) j.next(); currentBranchOutputs.put(outputName, processor.createOutput(outputName)); } } } /** * Those outputs that must be connected to an outer pipeline output */ public Set getOutputsByParamRef() { return outputsByParamRef; } public Set getOutputsById() { return outputsById; } @Override public ProcessorOutput createOutput(String name) { final String _name = name; final ProcessorOutput output = new ProcessorOutputImpl(ConcreteChooseProcessor.this, name) { public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) { final State state = (State) getState(context); if (!state.started) start(context); final ProcessorOutput branchOutput = state.selectedBranchOutputs.get(_name); branchOutput.read(context, xmlReceiver); } @Override public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) { if (isInputInCache(pipelineContext, AbstractChooseProcessor.CHOOSE_DATA_INPUT)) { final State state = (State) getState(pipelineContext); if (!state.started) start(pipelineContext); return state.selectedBranchOutputs.get(_name).getKey(pipelineContext); } else return null; } @Override protected Object getValidityImpl(PipelineContext pipelineContext) { if (isInputInCache(pipelineContext, AbstractChooseProcessor.CHOOSE_DATA_INPUT)) { final State state = (State) getState(pipelineContext); if (!state.started) start(pipelineContext); return state.selectedBranchOutputs.get(_name).getValidity(pipelineContext); } else return null; } }; addOutput(name, output); return output; } @Override public void start(PipelineContext pipelineContext) { final State state = (State) getState(pipelineContext); if (state.started) throw new IllegalStateException("ASTChoose Processor already started"); // Choose which branch we want to run (we cache the decision) DocumentInfo hrefDocumentInfo = null; int branchIndex = 0; int selectedBranch = -1; for (Iterator i = branchConditions.iterator(); i.hasNext(); branchIndex++) { // Evaluate expression final String condition = (String) i.next(); if (condition == null) { selectedBranch = branchIndex; break; } // LATER: Try to cache the XPath expressions. // Lazily read input in case there is only a p:otherwise if (hrefDocumentInfo == null) { final Configuration configuration = XPath.GlobalConfiguration(); hrefDocumentInfo = readCacheInputAsTinyTree(pipelineContext, configuration, AbstractChooseProcessor.CHOOSE_DATA_INPUT); } try { final NamespaceMapping namespaces = branchNamespaces.get(branchIndex); final PooledXPathExpression expression = XPathCache.getXPathExpression(hrefDocumentInfo.getConfiguration(), hrefDocumentInfo, "boolean(" + condition + ")", namespaces, null, org.orbeon.oxf.pipeline.api.FunctionLibrary.instance(), null, locationData);// TODO: location should be that of branch if (((Boolean) expression.evaluateSingleToJavaReturnToPoolOrNull()).booleanValue()) { selectedBranch = branchIndex; break; } } catch (Exception e) { if (logger.isDebugEnabled()) logger.debug("Choose: condition evaluation failed for condition: " + condition + " at " + branchProcessors.get(branchIndex));// TODO: location should be that of branch throw new ValidationException("Choose: condition evaluation failed for condition: " + condition, e, locationData);// TODO: location should be that of branch } } // In case the source document is large, and not cached, make it gc-able quicker (not sure if that makes a big difference!) hrefDocumentInfo = null; if (selectedBranch == -1) { // No branch was selected: this is not acceptable if there are output to the choose if (!outputsById.isEmpty() || !outputsByParamRef.isEmpty()) throw new ValidationException("Condition failed for every branch of choose: " + branchConditions.toString(), locationData); } else { // Initialize variables depending on selected branch final Processor selectedBranchProcessor = branchProcessors.get(selectedBranch); final Map<String, ProcessorInput> selectedBranchInputs = branchInputs.get(selectedBranch); state.selectedBranchOutputs = branchOutputs.get(selectedBranch); // Connect branch inputs for (Iterator<String> i = selectedBranchInputs.keySet().iterator(); i.hasNext();) { final String branchInputName = i.next(); final ProcessorInput branchInput = selectedBranchInputs.get(branchInputName); final ProcessorInput chooseInput = getInputByName(branchInputName); branchInput.setOutput(chooseInput.getOutput()); } // Connect branch outputs, or start processor selectedBranchProcessor.reset(pipelineContext); if (outputsById.size() == 0 && outputsByParamRef.size() == 0) { if (logger.isDebugEnabled()) { final String condition = (String) branchConditions.get(selectedBranch); // TODO: location should be that of branch if (condition != null) logger.debug("Choose: taking when branch with test: " + condition + " at " + locationData); else logger.debug("Choose: taking otherwise branch at " + locationData); } selectedBranchProcessor.start(pipelineContext); } state.started = true; } } @Override public void reset(PipelineContext context) { setState(context, new State()); for (final Processor processor : branchProcessors) processor.reset(context); } private static class State { public boolean started = false; public Map<Object, ProcessorOutput> selectedBranchOutputs; } }