/** * 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.xforms.processor; import org.orbeon.dom.Document; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.pipeline.api.TransformerXMLReceiver; import org.orbeon.oxf.processor.*; import org.orbeon.oxf.processor.impl.DependenciesProcessorInput; import org.orbeon.oxf.util.*; import org.orbeon.oxf.xforms.*; import org.orbeon.oxf.xforms.action.XFormsAPI; import org.orbeon.oxf.xforms.analysis.Metadata; import org.orbeon.oxf.xforms.analysis.XFormsAnnotator; import org.orbeon.oxf.xforms.analysis.XFormsExtractor; import org.orbeon.oxf.xforms.analysis.model.Instance; import org.orbeon.oxf.xforms.analysis.model.Model; import org.orbeon.oxf.xforms.model.XFormsModel; import org.orbeon.oxf.xforms.state.AnnotatedTemplate; import org.orbeon.oxf.xforms.state.XFormsStateManager; import org.orbeon.oxf.xforms.state.XFormsStaticStateCache; import org.orbeon.oxf.xml.*; import org.orbeon.oxf.xml.dom4j.LocationDocumentResult; import org.xml.sax.SAXException; import scala.Option; import javax.xml.transform.stream.StreamResult; import java.io.IOException; import java.util.Set; /** * This processor handles XForms initialization and produces an XHTML document which is a * translation from the source XForms + XHTML. */ abstract public class XFormsToSomething extends ProcessorImpl { private static final String INPUT_ANNOTATED_DOCUMENT = "annotated-document"; private static final String OUTPUT_DOCUMENT = "document"; public XFormsToSomething() { addInputInfo(new ProcessorInputOutputInfo(INPUT_ANNOTATED_DOCUMENT)); addInputInfo(new ProcessorInputOutputInfo("namespace")); // This input ensures that we depend on a portlet namespace addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DOCUMENT)); } /** * Case where an XML response must be generated. */ @Override public ProcessorOutput createOutput(final String outputName) { final ProcessorOutput output = new URIProcessorOutputImpl(XFormsToSomething.this, outputName, INPUT_ANNOTATED_DOCUMENT) { public void readImpl(final PipelineContext pipelineContext, XMLReceiver xmlReceiver) { doIt(pipelineContext, xmlReceiver, this, outputName); } @Override protected boolean supportsLocalKeyValidity() { return true; } @Override public KeyValidity getLocalKeyValidity(PipelineContext pipelineContext, URIReferences uriReferences) { // NOTE: As of 2010-03, caching of the output should never happen // - more work is needed to make this work properly // - not many use cases benefit return null; } }; addOutput(outputName, output); return output; } @Override public ProcessorInput createInput(final String inputName) { if (inputName.equals(INPUT_ANNOTATED_DOCUMENT)) { // Insert processor on the fly to handle dependencies. This is a bit tricky: we used to have an // XSLT/XInclude before XFormsToXHTML. This step handled XBL dependencies. Now that it is removed, we // need a mechanism to detect dependencies. So we insert a step here. // Return an input which handles dependencies // The system actually has two processors: // - stage1 is the processor automatically inserted below for the purpose of handling dependencies // - stage2 is the actual oxf:xforms-to-xhtml which actually does XForms processing final ProcessorInput originalInput = super.createInput(inputName); return new DependenciesProcessorInput(XFormsToSomething.this, inputName, originalInput) { @Override protected URIProcessorOutputImpl.URIReferences getURIReferences(PipelineContext pipelineContext) { // Return dependencies object, set by stage2 before reading its input return ((Stage2TransientState) XFormsToSomething.this.getState(pipelineContext)).stage1CacheableState; } }; } else { return super.createInput(inputName); } } @Override public void reset(PipelineContext context) { setState(context, new Stage2TransientState()); } // State passed by the second stage to the first stage. // NOTE: This extends URIReferencesState because we use URIProcessorOutputImpl. // It is not clear that we absolutely need URIProcessorOutputImpl in the second stage, but right now we keep it, // because XFormsURIResolver requires URIProcessorOutputImpl. private class Stage2TransientState extends URIProcessorOutputImpl.URIReferencesState { public Stage1CacheableState stage1CacheableState; } private void doIt(final PipelineContext pipelineContext, final XMLReceiver xmlReceiver, final URIProcessorOutputImpl processorOutput, String outputName) { final ExternalContext externalContext = NetUtils.getExternalContext(); final IndentedLogger htmlLogger = Loggers.getIndentedLogger("html"); final IndentedLogger cachingLogger = Loggers.getIndentedLogger("cache"); final XFormsStaticStateCache.CacheTracer cacheTracer; final boolean initializeXFormsDocument; { final XFormsStaticStateCache.CacheTracer customTracer = (XFormsStaticStateCache.CacheTracer) pipelineContext.getAttribute("orbeon.cache.test.tracer"); if (customTracer != null) cacheTracer = customTracer; else cacheTracer = new LoggingCacheTracer(cachingLogger); final Boolean initializeXFormsDocumentOrNull = (Boolean) pipelineContext.getAttribute("orbeon.cache.test.initialize-xforms-document"); initializeXFormsDocument = initializeXFormsDocumentOrNull != null ? initializeXFormsDocumentOrNull : true; } // ContainingDocument and XFormsState created below final XFormsContainingDocument[] containingDocument = new XFormsContainingDocument[1]; final boolean[] cachedStatus = new boolean[] { false } ; // Read and try to cache the complete XForms+XHTML document with annotations final Stage2CacheableState stage2CacheableState = readCacheInputAsObject(pipelineContext, getInputByName(INPUT_ANNOTATED_DOCUMENT), new CacheableInputReader<Stage2CacheableState>() { public Stage2CacheableState read(PipelineContext pipelineContext, ProcessorInput processorInput) { // Compute annotated XForms document + static state document final Stage1CacheableState stage1CacheableState = new Stage1CacheableState(); final Stage2CacheableState stage2CacheableState; final XFormsStaticState[] staticState = new XFormsStaticState[1]; { // Store dependencies container in state before reading ((Stage2TransientState) XFormsToSomething.this.getState(pipelineContext)).stage1CacheableState = stage1CacheableState; // Read static state from input stage2CacheableState = readStaticState(pipelineContext, cachingLogger, cacheTracer, staticState); } // Create containing document and initialize XForms engine // NOTE: Create document here so we can do appropriate analysis of caching dependencies final XFormsURIResolver uriResolver = new XFormsURIResolver(XFormsToSomething.this, processorOutput, pipelineContext, INPUT_ANNOTATED_DOCUMENT, XMLParsing.ParserConfiguration.PLAIN); containingDocument[0] = new XFormsContainingDocument( staticState[0], uriResolver, PipelineResponse.getResponse(xmlReceiver, externalContext), initializeXFormsDocument ); // Gather set caching dependencies gatherInputDependencies(containingDocument[0], cachingLogger, stage1CacheableState); return stage2CacheableState; } @Override public void foundInCache() { cachedStatus[0] = true; } }); try { // Create containing document if not done yet if (containingDocument[0] == null) { assert cachedStatus[0]; // In this case, we found the static state digest and more in the cache, but we must now create a new XFormsContainingDocument from this information cacheTracer.digestAndTemplateStatus(scala.Option.apply(stage2CacheableState.staticStateDigest)); final XFormsStaticState staticState; { final XFormsStaticState cachedState = XFormsStaticStateCache.getDocumentJava(stage2CacheableState.staticStateDigest); if (cachedState != null && cachedState.topLevelPart().metadata().bindingsIncludesAreUpToDate()) { // Found static state in cache cacheTracer.staticStateStatus(true, cachedState.digest()); staticState = cachedState; } else { // Not found static state in cache OR it is out of date, create static state from input // NOTE: In out of date case, could clone static state and reprocess instead? if (cachedState != null) cachingLogger.logDebug("", "out-of-date static state by digest in cache due to: " + cachedState.topLevelPart().metadata().debugOutOfDateBindingsIncludesJava()); final StaticStateBits staticStateBits = new StaticStateBits(pipelineContext, cachingLogger, stage2CacheableState.staticStateDigest); staticState = XFormsStaticStateImpl.createFromStaticStateBits(staticStateBits.staticStateDocument, stage2CacheableState.staticStateDigest, staticStateBits.metadata, staticStateBits.template); cacheTracer.staticStateStatus(false, staticState.digest()); // Store in cache XFormsStaticStateCache.storeDocument(staticState); } } final XFormsURIResolver uriResolver = new XFormsURIResolver(XFormsToSomething.this, processorOutput, pipelineContext, INPUT_ANNOTATED_DOCUMENT, XMLParsing.ParserConfiguration.PLAIN); containingDocument[0] = new XFormsContainingDocument( staticState, uriResolver, PipelineResponse.getResponse(xmlReceiver, externalContext), initializeXFormsDocument ); } else { assert !cachedStatus[0]; cacheTracer.digestAndTemplateStatus(Option.<String>apply(null)); } // Output resulting document if (initializeXFormsDocument) produceOutput(pipelineContext, outputName, externalContext, htmlLogger, stage2CacheableState, containingDocument[0], xmlReceiver); // Notify state manager XFormsAPI.withContainingDocumentJava(containingDocument[0], new Runnable() { // scope because dynamic properties can cause lazy XPath evaluations public void run() { XFormsStateManager.instance().afterInitialResponse(containingDocument[0], stage2CacheableState.template); } }); } catch (Throwable e) { htmlLogger.logDebug("", "throwable caught during initialization."); throw new OXFException(e); } } abstract protected void produceOutput( PipelineContext pipelineContext, String outputName, ExternalContext externalContext, IndentedLogger indentedLogger, Stage2CacheableState stage2CacheableState, XFormsContainingDocument containingDocument, XMLReceiver xmlReceiver) throws IOException, SAXException; private Stage2CacheableState readStaticState( PipelineContext pipelineContext, IndentedLogger logger, XFormsStaticStateCache.CacheTracer cacheTracer, XFormsStaticState[] staticState) { final StaticStateBits staticStateBits = new StaticStateBits(pipelineContext, logger, null); { final XFormsStaticState cachedState = XFormsStaticStateCache.getDocumentJava(staticStateBits.staticStateDigest); if (cachedState != null && cachedState.topLevelPart().metadata().bindingsIncludesAreUpToDate()) { // Found static state in cache cacheTracer.staticStateStatus(true, cachedState.digest()); staticState[0] = cachedState; } else { // Not found static state in cache OR it is out of date, create and initialize static state object // NOTE: In out of date case, could clone static state and reprocess instead? if (cachedState != null) logger.logDebug("", "out-of-date static state by digest in cache due to: " + cachedState.topLevelPart().metadata().debugOutOfDateBindingsIncludesJava()); staticState[0] = XFormsStaticStateImpl.createFromStaticStateBits(staticStateBits.staticStateDocument, staticStateBits.staticStateDigest, staticStateBits.metadata, staticStateBits.template); cacheTracer.staticStateStatus(false, staticState[0].digest()); // Store in cache XFormsStaticStateCache.storeDocument(staticState[0]); } } // Update input dependencies object return new Stage2CacheableState(staticStateBits.staticStateDigest, staticStateBits.template); } private class StaticStateBits { private final boolean isLogStaticStateInput = XFormsProperties.getDebugLogging().contains("html-static-state"); public final Metadata metadata = new Metadata(); public final Document staticStateDocument; public final AnnotatedTemplate template; public final String staticStateDigest; public StaticStateBits(PipelineContext pipelineContext, IndentedLogger logger, String existingStaticStateDigest) { final boolean computeDigest = isLogStaticStateInput || existingStaticStateDigest == null; logger.startHandleOperation("", "reading input", "existing digest", existingStaticStateDigest); final TransformerXMLReceiver documentReceiver = TransformerUtils.getIdentityTransformerHandler(); final LocationDocumentResult documentResult = new LocationDocumentResult(); documentReceiver.setResult(documentResult); final DigestContentHandler digestReceiver = computeDigest ? new DigestContentHandler() : null; final XMLReceiver extractorOutput; if (isLogStaticStateInput) { extractorOutput = computeDigest ? new TeeXMLReceiver(documentReceiver, digestReceiver, getDebugReceiver(logger)) : new TeeXMLReceiver(documentReceiver, getDebugReceiver(logger)); } else { extractorOutput = computeDigest ? new TeeXMLReceiver(documentReceiver, digestReceiver) : documentReceiver; } // Read the input through the annotator and gather namespace mappings // // Output of annotator is: // // - annotated page template (TODO: this should not include model elements) // - extractor // // Output of extractor is: // // - static state document // - optionally: digest // - optionally: debug output // this.template = AnnotatedTemplate.applyJava(new SAXStore()); readInputAsSAX(pipelineContext, INPUT_ANNOTATED_DOCUMENT, new WhitespaceXMLReceiver( new XFormsAnnotator( this.template.saxStore(), new XFormsExtractor( scala.Option.<XMLReceiver>apply( new WhitespaceXMLReceiver( extractorOutput, WhitespaceMatching.defaultBasePolicy(), WhitespaceMatching.basePolicyMatcher() ) ), metadata, scala.Option.<AnnotatedTemplate>apply(template), ".", XFormsConstants.XXBLScope.inner, true, false ), metadata, true ), WhitespaceMatching.defaultHTMLPolicy(), WhitespaceMatching.htmlPolicyMatcher() )); this.staticStateDocument = documentResult.getDocument(); this.staticStateDigest = computeDigest ? NumberUtils.toHexString(digestReceiver.getResult()) : null; assert !isLogStaticStateInput || existingStaticStateDigest == null || this.staticStateDigest.equals(existingStaticStateDigest); logger.endHandleOperation("computed digest", this.staticStateDigest); } private XMLReceiver getDebugReceiver(final IndentedLogger indentedLogger) { final TransformerXMLReceiver identity = TransformerUtils.getIdentityTransformerHandler(); final StringBuilderWriter writer = new StringBuilderWriter(); identity.setResult(new StreamResult(writer)); return new ForwardingXMLReceiver(identity) { @Override public void endDocument() throws SAXException { super.endDocument(); // Log out at end of document indentedLogger.logDebug("", "static state input", "input", writer.toString()); } }; } } // What can be cached by the first stage: URI dependencies private static class Stage1CacheableState extends URIProcessorOutputImpl.URIReferences {} // What can be cached by the second stage: SAXStore and static state public static class Stage2CacheableState extends URIProcessorOutputImpl.URIReferences { public final String staticStateDigest; public final AnnotatedTemplate template; public Stage2CacheableState(String staticStateDigest, AnnotatedTemplate template) { this.staticStateDigest = staticStateDigest; this.template = template; } } private void gatherInputDependencies(XFormsContainingDocument containingDocument, IndentedLogger logger, Stage1CacheableState stage1CacheableState) { // Add static instance source dependencies for top-level models // TODO: check all models/instances final PartAnalysis topLevelPart = containingDocument.getStaticState().topLevelPart(); for (final Model model : topLevelPart.jGetModelsForScope(topLevelPart.startScope())) { for (final Instance instance: model.instancesMap().values()) { if (instance.dependencyURL().isDefined()) { final String resolvedDependencyURL = XFormsUtils.resolveServiceURL(containingDocument, instance.element(), instance.dependencyURL().get(), ExternalContext.Response.REWRITE_MODE_ABSOLUTE); if (!instance.cache()) { stage1CacheableState.addReference(null, resolvedDependencyURL, instance.credentialsOrNull()); if (logger.isDebugEnabled()) logger.logDebug("", "adding document cache dependency for non-cacheable instance", "instance URI", resolvedDependencyURL); } else { // Don't add the dependency as we don't want the instance URI to be hit // For all practical purposes, globally shared instances must remain constant! if (logger.isDebugEnabled()) logger.logDebug("", "not adding document cache dependency for cacheable instance", "instance URI", resolvedDependencyURL); } } } } // Set caching dependencies if the input was actually read // TODO: check all models/instances // Q: should use static dependency information instead? what about schema imports and instance replacements? for (final XFormsModel currentModel: containingDocument.getModelsJava()) { // Add schema dependencies final String[] schemaURIs = currentModel.getSchemaURIs(); // TODO: We should also use dependencies computed in XFormsModelSchemaValidator.SchemaInfo if (schemaURIs != null) { for (final String currentSchemaURI: schemaURIs) { if (logger.isDebugEnabled()) logger.logDebug("", "adding document cache dependency for schema", "schema URI", currentSchemaURI); stage1CacheableState.addReference(null, currentSchemaURI, null);// TODO: support credentials on schema refs } } } // TODO: Add @src attributes from controls? Not used often. // Set caching dependencies for XBL inclusions { final Metadata metadata = containingDocument.getStaticState().topLevelPart().metadata(); final Set<String> includes = metadata.getBindingIncludesJava(); for (final String include : includes) { stage1CacheableState.addReference(null, "oxf:" + include, null); } } } }