/** * 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.impl; import org.orbeon.dom.DocumentFactory; import org.orbeon.dom.Element; import org.orbeon.oxf.cache.OutputCacheKey; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.common.OrbeonLocationException; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.processor.*; import org.orbeon.oxf.processor.generator.DOMGenerator; import org.orbeon.oxf.processor.validation.MSVValidationProcessor; import org.orbeon.oxf.properties.Properties; import org.orbeon.oxf.properties.PropertySet; import org.orbeon.oxf.util.PipelineUtils; import org.orbeon.oxf.xml.InspectingXMLReceiver; import org.orbeon.oxf.xml.XMLConstants; import org.orbeon.oxf.xml.XMLReceiver; import org.orbeon.oxf.xml.XMLReceiverHelper; import org.orbeon.oxf.xml.dom4j.LocationData; /** * Base class for all built-in ProcessorOutput implementations. */ public abstract class ProcessorOutputImpl implements ProcessorOutput { private final Processor processor; private final Class processorClass; private final String name; private ProcessorInput input; private String id; private String schema; private String debugMessage; private LocationData locationData; public ProcessorOutputImpl(Class processorClass, String name) { this.processor = null; this.processorClass = processorClass; this.name = name; } public ProcessorOutputImpl(Processor processor, String name) { assert processor != null; this.processor = processor; this.processorClass = processor.getClass(); this.name = name; } public Processor getProcessor(PipelineContext pipelineContext) { return processor; } public void setInput(ProcessorInput input) { this.input = input; } public ProcessorInput getInput() { return input; } public Class getProcessorClass() { return processorClass; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public String getSchema() { return schema; } public void setSchema(String schema) { this.schema = schema; } public String getDebugMessage() { return debugMessage; } public LocationData getLocationData() { return locationData; } public void setDebug(String debugMessage) { this.debugMessage = debugMessage; } public void setLocationData(LocationData locationData) { this.locationData = locationData; } /** * Read method that subclasses must implement. * * @param pipelineContext context * @param xmlReceiver receiver */ protected abstract void readImpl(PipelineContext pipelineContext, XMLReceiver xmlReceiver); /** * Get the cache key. May be implemented by subclass. * * @param pipelineContext context * @return cache key or null */ protected OutputCacheKey getKeyImpl(PipelineContext pipelineContext) { return null; } /** * Get the cache validity. May be implemented by subclass. * * @param pipelineContext context * @return cache validity or null */ protected Object getValidityImpl(PipelineContext pipelineContext) { return null; } private abstract class RuntimeOutputFilter implements ProcessorOutput { public Processor getProcessor(PipelineContext pipelineContext) { return ProcessorOutputImpl.this.getProcessor(pipelineContext); } // None of the methods implemented here should be called public void setInput(ProcessorInput processorInput) { throw new OXFException("This method should never be called!"); } public ProcessorInput getInput() { throw new OXFException("This method should never be called!"); } public void setSchema(String schema) { throw new OXFException("This method should never be called!"); } public String getSchema() { throw new OXFException("This method should never be called!"); } public Class getProcessorClass() { throw new OXFException("This method should never be called!"); } public String getId() { throw new OXFException("This method should never be called!"); } public String getName() { throw new OXFException("This method should never be called!"); } public void setDebug(String debugMessage) { throw new OXFException("This method should never be called!"); } public String getDebugMessage() { throw new OXFException("This method should never be called!"); } public void setLocationData(LocationData locationData) { throw new OXFException("This method should never be called!"); } public LocationData getLocationData() { throw new OXFException("This method should never be called!"); } public void read(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { throw new OXFException("This method should never be called!"); } } /** * Constructor takes: (1) the input and output of a processor and (2) a * "previousOutput". It connects the input to the previousOutput and * when read, reads the output. * * Semantic: creates an output that is just like previousOutput on which * a processor is applies. */ private class ConcreteRuntimeOutputFilter extends RuntimeOutputFilter { private ProcessorOutput processorOutput; private ProcessorOutput previousProcessorOutput; private class ForwarderRuntimeOutputOutput extends RuntimeOutputFilter { @Override public void read(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { previousProcessorOutput.read(pipelineContext, xmlReceiver); } public OutputCacheKey getKey(PipelineContext pipelineContext) { return previousProcessorOutput.getKey(pipelineContext); } public Object getValidity(PipelineContext pipelineContext) { return previousProcessorOutput.getValidity(pipelineContext); } } public ConcreteRuntimeOutputFilter(ProcessorInput processorInput, ProcessorOutput processorOutput, final ProcessorOutput previousOutput) { this.processorOutput = processorOutput; this.previousProcessorOutput = previousOutput; processorInput.setOutput(new ForwarderRuntimeOutputOutput()); } @Override public void read(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { processorOutput.read(pipelineContext, xmlReceiver); } public OutputCacheKey getKey(PipelineContext pipelineContext) { return processorOutput.getKey(pipelineContext); } public Object getValidity(PipelineContext pipelineContext) { return processorOutput.getValidity(pipelineContext); } } private class TopLevelOutputFilter extends RuntimeOutputFilter { @Override public void read(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { // Read the current processor output readImpl(pipelineContext, xmlReceiver); } public OutputCacheKey getKey(PipelineContext pipelineContext) { return getKeyImpl(pipelineContext); } public Object getValidity(PipelineContext pipelineContext) { return getValidityImpl(pipelineContext); } }; private RuntimeOutputFilter createFilter() { // Get inspector instance // Final filter (i.e. at the top, executed last) RuntimeOutputFilter outputFilter = new TopLevelOutputFilter(); // Handle debug if (getDebugMessage() != null || (getInput() != null && getInput().getDebugMessage() != null)) { ProcessorFactory debugProcessorFactory = ProcessorFactoryRegistry.lookup(XMLConstants.DEBUG_PROCESSOR_QNAME); if (debugProcessorFactory == null) throw new OXFException("Cannot find debug processor factory for QName: " + XMLConstants.DEBUG_PROCESSOR_QNAME); for (int i = 0; i < 2; i++) { String debugMessage = i == 0 ? getDebugMessage() : getInput() == null ? null : getInput().getDebugMessage(); LocationData debugLocationData = i == 0 ? getLocationData() : getInput() == null ? null : getInput().getLocationData(); if (debugMessage != null) { Processor debugProcessor = debugProcessorFactory.createInstance(); debugProcessor.createInput(ProcessorImpl.INPUT_DATA); debugProcessor.createOutput(ProcessorImpl.OUTPUT_DATA); // Create config document for Debug processor final org.orbeon.dom.Document debugConfigDocument; { debugConfigDocument = DocumentFactory.createDocument(); Element configElement = debugConfigDocument.addElement("config"); configElement.addElement("message").addText(debugMessage); if (debugLocationData != null) { Element systemIdElement = configElement.addElement("system-id"); if (debugLocationData.file() != null) systemIdElement.addText(debugLocationData.file()); configElement.addElement("line").addText(Integer.toString(debugLocationData.line())); configElement.addElement("column").addText(Integer.toString(debugLocationData.col())); } } final DOMGenerator dg = new DOMGenerator ( debugConfigDocument, "debug filter" , DOMGenerator.ZeroValidity, DOMGenerator.DefaultContext ); PipelineUtils.connect( dg, "data", debugProcessor, "config"); final ProcessorOutput dbgOut = debugProcessor.getOutputByName( ProcessorImpl.OUTPUT_DATA ); final ProcessorInput dbgIn = debugProcessor.getInputByName( ProcessorImpl.INPUT_DATA ); outputFilter = new ConcreteRuntimeOutputFilter( dbgIn, dbgOut, outputFilter); } } } // The PropertySet can be null during properties initialization. This should be one of the // rare places where this should be tested on. final PropertySet propertySet = Properties.instance().getPropertySet(); // Create and hook-up output validation processor if needed final Boolean isUserValidation = (propertySet == null) ? null : propertySet.getBoolean(ProcessorImpl.USER_VALIDATION_FLAG, true); if (isUserValidation != null && isUserValidation.booleanValue() && getSchema() != null) { final Processor outputValidator = new MSVValidationProcessor(getSchema()); // Create data input and output final ProcessorInput input = outputValidator.createInput(ProcessorImpl.INPUT_DATA); final ProcessorOutput output = outputValidator.createOutput(ProcessorImpl.OUTPUT_DATA); // Create and connect config input final Processor resourceGenerator = PipelineUtils.createURLGenerator(getSchema()); PipelineUtils.connect(resourceGenerator, ProcessorImpl.OUTPUT_DATA, outputValidator, MSVValidationProcessor.INPUT_SCHEMA); PipelineUtils.connect(MSVValidationProcessor.NO_DECORATION_CONFIG, ProcessorImpl.OUTPUT_DATA, outputValidator, ProcessorImpl.INPUT_CONFIG); outputFilter = new ConcreteRuntimeOutputFilter(input, output, outputFilter); } // Hook-up input validation processor if needed if (isUserValidation != null && isUserValidation.booleanValue() && getInput() != null && getInput().getSchema() != null) { final Processor inputValidator = new MSVValidationProcessor(getInput().getSchema()); // Create data input and output final ProcessorInput input = inputValidator.createInput(ProcessorImpl.INPUT_DATA); final ProcessorOutput output = inputValidator.createOutput(ProcessorImpl.OUTPUT_DATA); // Create and connect config input final Processor resourceGenerator = PipelineUtils.createURLGenerator(getInput().getSchema()); PipelineUtils.connect(resourceGenerator, ProcessorImpl.OUTPUT_DATA, inputValidator, MSVValidationProcessor.INPUT_SCHEMA); PipelineUtils.connect(MSVValidationProcessor.NO_DECORATION_CONFIG, ProcessorImpl.OUTPUT_DATA, inputValidator, ProcessorImpl.INPUT_CONFIG); outputFilter = new ConcreteRuntimeOutputFilter(input, output, outputFilter); } // Perform basic inspection of SAX events Boolean isSAXInspection = (propertySet == null) ? null : propertySet.getBoolean(ProcessorImpl.SAX_INSPECTION_FLAG, false); if (isSAXInspection != null && isSAXInspection.booleanValue()) { final RuntimeOutputFilter previousOutputFilter = outputFilter; outputFilter = new RuntimeOutputFilter() { @Override public void read(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { InspectingXMLReceiver inspectingXMLReceiver = new InspectingXMLReceiver(xmlReceiver); previousOutputFilter.read(pipelineContext, inspectingXMLReceiver); } public OutputCacheKey getKey(PipelineContext pipelineContext) { return previousOutputFilter.getKey(pipelineContext); } public Object getValidity(PipelineContext pipelineContext) { return previousOutputFilter.getValidity(pipelineContext); } }; } return outputFilter; } /** * NOTE: We should never use processor instance variables. Here, the creation may not be thread safe in that * the filter may be initialized several times. This should not be a real problem, and the execution should not * be problematic either. It may be safer to synchronize getRuntimeFilter(). */ private RuntimeOutputFilter outputFilter = null; private RuntimeOutputFilter getRuntimeFilter() { if (outputFilter == null) outputFilter = createFilter(); return outputFilter; } public final void read(PipelineContext pipelineContext, XMLReceiver xmlReceiver) { try { // Delegate getRuntimeFilter().read(pipelineContext, xmlReceiver); // NOTE: Not sure why we used to catch and log AbstractMethodError here, but we should not! } catch (Exception e) { throw OrbeonLocationException.wrapException(e, getLocationData()); } } public final OutputCacheKey getKey(PipelineContext pipelineContext) { return getRuntimeFilter().getKey(pipelineContext); // // In debug mode, force read of input if not read yet // if (pipelineContext.isKeyDebugging() && pipelineContext.getTraceForUpdate() != null) { // final TraceEntry traceEntry = pipelineContext.getTraceForUpdate().getTraceEntry(this); // if (!traceEntry.outputReadCalled) // read(pipelineContext, new XMLReceiverAdapter()); // } // // // Delegate // final OutputCacheKey outputCacheKey = getRuntimeFilter().getKey(pipelineContext); // // // Update trace if needed // if (pipelineContext.isKeyDebugging() && pipelineContext.getTraceForUpdate() != null) { // final TraceEntry traceEntry = pipelineContext.getTraceForUpdate().getTraceEntry(this); // traceEntry.outputGetKeyCalled(outputCacheKey == null); // } // // return outputCacheKey; } public final Object getValidity(PipelineContext pipelineContext) { return getRuntimeFilter().getValidity(pipelineContext); } public void toXML(PipelineContext pipelineContext, XMLReceiverHelper helper) { final ProcessorInput input = getInput(); // Input connected to the output if (input != null) { helper.startElement("input", new String[] { "name", input.getName(), "processor-class", input.getProcessorClass() != null ? input.getProcessorClass().getName() : null, "processor-name", input.getProcessor(pipelineContext) != null && input.getProcessor(pipelineContext).getName() != null ? input.getProcessor(pipelineContext).getName().getQualifiedName() : null, "processor-object", input.getProcessor(pipelineContext) != null ? Integer.toString(input.getProcessor(pipelineContext).getSequenceNumber()) : null, "system-id", (input.getLocationData() != null) ? input.getLocationData().file() : null, "line", (input.getLocationData() != null) ? Integer.toString(input.getLocationData().line()) : null, "column", (input.getLocationData() != null) ? Integer.toString(input.getLocationData().col()) : null }); } // Output connected to the input helper.element("output", new String[] { "id", getId(), "name", getName(), "processor-class", getProcessorClass() != null ? getProcessorClass().getName() : null, "processor-name", getProcessor(pipelineContext) != null && getProcessor(pipelineContext).getName() != null ? getProcessor(pipelineContext).getName().getQualifiedName() : null, "processor-object", getProcessor(pipelineContext) != null ? Integer.toString(getProcessor(pipelineContext).getSequenceNumber()) : null, "system-id", (getLocationData() != null) ? getLocationData().file() : null, "line", (getLocationData() != null) ? Integer.toString(getLocationData().line()) : null, "column", (getLocationData() != null) ? Integer.toString(getLocationData().col()) : null, "schema", getSchema(), "debug", getDebugMessage() }); if (input != null) { helper.endElement(); } } }