/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.cocoon.transformation; import org.apache.avalon.framework.CascadingRuntimeException; import org.apache.avalon.framework.component.WrapperComponentManager; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.logger.Logger; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.Processor; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.components.CocoonComponentManager; import org.apache.cocoon.components.source.SourceUtil; import org.apache.cocoon.components.source.impl.MultiSourceValidity; import org.apache.cocoon.components.thread.RunnableManager; import org.apache.cocoon.environment.Environment; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.transformation.helpers.NOPRecorder; import org.apache.cocoon.util.NetUtils; import org.apache.cocoon.xml.AbstractXMLPipe; import org.apache.cocoon.xml.IncludeXMLConsumer; import org.apache.cocoon.xml.NamespacesTable; import org.apache.cocoon.xml.SaxBuffer; import org.apache.cocoon.xml.XMLConsumer; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceValidity; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import java.util.Stack; /** * <p>A simple transformer including resolvable sources (accessed through * Cocoon's {@link SourceResolver}) from its input.</p> * * <p>Inclusion is triggered by the <code><include ... /></code> element * defined in the <code>http://apache.org/cocoon/include/1.0</code> namespace.</p> * * <p>Example:</p> * <pre> * <i:include xmlns:i="http://apache.org/cocoon/include/1.0" * src="cocoon://path/to/include"/> * </pre> * * <p>An interesting feature of this {@link Transformer} is that it implements the * {@link CacheableProcessingComponent} interface and provides full support for * caching. In other words, if the input given to this transformer has not changed, * and all of the included sources are (cacheable) and still valid, this transformer * will not force a pipeline re-generation like the {@link CIncludeTransformer}.</p> * * * <h3>Relative Source Resolution</h3> * <p>Include sources which are specified using relative URI will be resolved * relative to the source document location. This is consistent with * {@link XIncludeTransformer} behavior, but differs from {@link CIncludeTransformer}. * * <h3>Root Element Stripping</h3> * <p>The root element of included content may be automatically stripped by specifying * <code>strip-root="true"</code> on the <code>include</code> element. This is the same * functionality as provided by {@link org.apache.cocoon.transformation.CIncludeTransformer}. * Setting <ode>strip-root="true"</code> has no effect if recursive processing is enabled. * </p> * * <p>Example:</p> * <pre> * <i:include xmlns:i="http://apache.org/cocoon/include/1.0" * src="cocoon://path/to/include" strip-root="true"/> * </pre> * * <h3>Parameters Passing</h3> * <p>Parameters to be passed to the included sources can be specified in two ways: * the first one is to encode them onto the source itelf, for example:</p> * * <pre> * <i:include xmlns:i="http://apache.org/cocoon/include/1.0" * src="cocoon://path/to/include?paramA=valueA&paramB=valueB"/> * </pre> * * <p>Another approach allows the encoding of parameters to be done automatically by * the transformer, so that one can easily pass parameter name or values containing * the <code>&</code> (amperstand) or <code>=</code> (equals) character, which are * reserved characters in URIs. An example:</p> * * <pre> * <i:include xmlns:i="http://apache.org/cocoon/include/1.0" * src="cocoon://path/to/include"> * <i:parameter name="firstParameterName" value="firstParameterValue"/> * <i:parameter name="other&Para=Name" value="other=Para&Value"/> * </i:include> * </pre> * * * <h3>Fallback Element</h3> * <p>IncludeTransformer allows fallback element to be specified within * include element. XML content of the fallback element will be included instead * of source content if source inclusion caused an exception. Fallback element * can have nested include elements. An example:</p> * * <pre> * <i:include xmlns:i="http://apache.org/cocoon/include/1.0" * src="cocoon://path/to/include"> * <i:fallback> * <strong>The data is temporarily unavailable.</strong> * We are sorry for the trouble; please try again later. * </i:fallback> * </i:include> * </pre> * * * <h3>Parallel Processing</h3> * <p>Another feature of this {@link Transformer} is that it allows parallel processing * of includes. By setting the optional parameter <code>parallel</code> to true, * the various included contents are processed (included) in parallel threads rather * than in series, in one thread. This parameter can be set in either the transformer * definition (to affect all IncludeTransformer instances):</p> * <pre> * <parallel>true</parallel> * </pre> * * <p>or in a pipeline itself (to only affect that instance of the IncludeTransformer):</p> * <pre> * <map:parameter name="parallel" value="true"/> * </pre> * <p>By default, parallel processing is turned off.</p> * * * <h3>Recursive Processing</h3> * <p>This {@link Transformer} allows recursive processing of includes. * By setting the optional parameter <code>recursive</code> to true, * the various included contents are scanned for include elements, and processed * in the same manner as incoming XML events. This parameter can be set in either * the transformer definition (to affect all IncludeTransformer instances):</p> * <pre> * <recursive>true</recursive> * </pre> * * <p>or in a pipeline itself (to only affect that instance of the IncludeTransformer):</p> * <pre> * <map:parameter name="recursive" value="true"/> * </pre> * <p>This feature is similar to the XInclude processing. By default, * recursive processing is turned off.</p> * * * @cocoon.sitemap.component.documentation * A simple transformer including resolvable sources (accessed through * Cocoon's SourceResolver) from its input. * * @cocoon.sitemap.component.name include * @cocoon.sitemap.component.logger sitemap.transformer.include * @cocoon.sitemap.component.pooling.max 16 * @version $Id$ */ public class IncludeTransformer extends AbstractTransformer implements Serviceable, Configurable, CacheableProcessingComponent { /** <p>The namespace URI of the elements recognized by this transformer.</p> */ private static final String NS_URI = "http://apache.org/cocoon/include/1.0"; /** <p>The name of the element triggering inclusion of sources.</p> */ private static final String INCLUDE_ELEMENT = "include"; /** <p>The name of the element defining a fallback content.</p> */ private static final String FALLBACK_ELEMENT = "fallback"; /** <p>The name of the element defining an included subrequest parameter.</p> */ private static final String PARAMETER_ELEMENT = "parameter"; /** <p>The name of the attribute indicating the included source URI.</p> */ private static final String SRC_ATTRIBUTE = "src"; /** <p>The name of the mime type attribute containing the hint for the {@link org.apache.excalibur.xmlizer.XMLizer}.</p> */ private static final String MIME_ATTRIBUTE = "mime-type"; /** <p>The name of the parse attribute indicating type of included source processing: xml or text.</p> */ private static final String PARSE_ATTRIBUTE = "parse"; /** <p>The name of the strip-root attribute indicating that the root element of included xml source should be stripped.</p> */ private static final String STRIP_ROOT_ATTRIBUTE = "strip-root"; /** <p>The name of the attribute indicating the parameter name.</p> */ private static final String NAME_ATTRIBUTE = "name"; /** <p>The name of the attribute indicating the parameter name.</p> */ private static final String VALUE_ATTRIBUTE = "value"; /** <p>The encoding to use for parameter names and values.</p> */ private static final String ENCODING = "US-ASCII"; // // Global configuration // /** The {@link ServiceManager} instance associated with this instance. */ protected ServiceManager manager; /** Configuration option controlling recursive includes processing */ private boolean defaultRecursive; /** Configuration option controlling parallel (in multiple threads) includes processing */ private boolean defaultParallel; /** Configuration option controlling parallel (in multiple threads) includes processing in the recursive includes */ private boolean defaultRecursiveParallel; /** The name of the thread pool to use (for parallel processing). */ protected String threadPool; /** The default value to be appended to the caching key. */ private String defaultKey; // // Current configuration // /** The {@link SourceResolver} used to resolve included URIs. */ protected SourceResolver resolver; /** The {@link Environment} used within parallel threads */ protected Environment environment; /** The {@link Processor} used within parallel threads */ private Processor processor; /** The value to be appended to the caching key. */ private String key; // // Current state // /** The {@link SourceValidity} instance associated with this request. */ protected MultiSourceValidity validity; /** A {@link NamespacesTable} used to filter namespace declarations. */ private NamespacesTable namespaces; /** The {@link IncludeXMLPipe} which is doing all the work */ private final IncludeXMLPipe pipe; /** * <p>Create a new {@link IncludeTransformer} instance.</p> */ public IncludeTransformer() { pipe = new IncludeXMLPipe(); } /** * <p>Initialize own and {@link #pipe} loggers</p> */ public void enableLogging(Logger logger) { super.enableLogging(logger); pipe.enableLogging(logger); } /** * <p>Setup the {@link ServiceManager} available for this instance.</p> * * @see Serviceable#service(ServiceManager) */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; } /* (non-Javadoc) * @see Configurable#configure(Configuration) */ public void configure(Configuration configuration) throws ConfigurationException { /* Read configuration nodes for recursive, parallel, recursive-parallel */ this.defaultRecursive = configuration.getChild("recursive").getValueAsBoolean(false); this.defaultParallel = configuration.getChild("parallel").getValueAsBoolean(false); this.defaultRecursiveParallel = configuration.getChild("recursive-parallel").getValueAsBoolean(false); /* Read configuration node for thread pool name */ this.threadPool = configuration.getChild("thread-pool").getValue("default"); this.defaultKey = configuration.getChild("key").getValue(null); } /** * <p>Setup this component instance in the context of its pipeline and * current request.</p> * * @see Serviceable#service(ServiceManager) */ public void setup(SourceResolver resolver, Map om, String src, Parameters parameters) throws ProcessingException, SAXException, IOException { /* Read sitemap parameters */ this.pipe.recursive = parameters.getParameterAsBoolean("recursive", this.defaultRecursive); this.pipe.parallel = parameters.getParameterAsBoolean("parallel", this.defaultParallel); this.pipe.recursiveParallel = parameters.getParameterAsBoolean("recursive-parallel", this.defaultRecursiveParallel); this.key = parameters.getParameter("key", this.defaultKey); /* Init transformer state */ if (this.pipe.parallel) { this.environment = CocoonComponentManager.getCurrentEnvironment(); this.processor = CocoonComponentManager.getCurrentProcessor(); } this.namespaces = new NamespacesTable(); this.resolver = resolver; this.validity = null; // Set root include pipe as consumer. // Won't use setter methods here - they are overridden super.xmlConsumer = pipe; super.contentHandler = pipe; super.lexicalHandler = pipe; } public void setConsumer(XMLConsumer consumer) { pipe.setConsumer(consumer); } public void setContentHandler(ContentHandler handler) { pipe.setContentHandler(handler); } public void setLexicalHandler(LexicalHandler handler) { pipe.setLexicalHandler(handler); } /** * <p>Recycle this component instance.</p> * * @see org.apache.avalon.excalibur.pool.Recyclable#recycle() */ public void recycle() { this.namespaces = null; this.validity = null; /* Make sure all threads completed their work */ this.pipe.recycle(); // Resolver can be nulled out when all threads completed processing // and released their Sources. this.resolver = null; super.recycle(); } /** * <p>Receive notification of the beginning of an XML document.</p> * * @see ContentHandler#startDocument */ public void startDocument() throws SAXException { /* Make sure that we have a validity while processing */ getValidity(); super.startDocument(); } /** * <p>Receive notification of the end of an XML document.</p> * * @see ContentHandler#startDocument() */ public void endDocument() throws SAXException { /* Make sure that the validity is "closed" at the end */ this.validity.close(); super.endDocument(); } /** * <p>Receive notification of the start of a prefix mapping.</p> * * <p>This transformer will remove all prefix mapping declarations for those * prefixes associated with the <code>http://apache.org/cocoon/include/1.0</code> * namespace.</p> * * @see org.xml.sax.ContentHandler#startPrefixMapping(String, String) */ public void startPrefixMapping(String prefix, String nsuri) throws SAXException { if (NS_URI.equals(nsuri)) { /* Skipping mapping for the current prefix as it's ours */ this.namespaces.addDeclaration(prefix, nsuri); } else { /* Map the current prefix, as we don't know it */ super.startPrefixMapping(prefix, nsuri); } } /** * <p>Receive notification of the end of a prefix mapping.</p> * * <p>This transformer will remove all prefix mapping declarations for those * prefixes associated with the <code>http://apache.org/cocoon/include/1.0</code> * namespace.</p> * * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String) */ public void endPrefixMapping(String prefix) throws SAXException { if (NS_URI.equals(this.namespaces.getUri(prefix))) { /* Skipping unmapping for the current prefix as it's ours */ this.namespaces.removeDeclaration(prefix); } else { /* Unmap the current prefix, as we don't know it */ super.endPrefixMapping(prefix); } } /** * <p>Return the caching key associated with this transformation.</p> * * <p>When including <code>cocoon://</code> sources with dynamic * content depending on environment (request parameters, session attributes, * etc), it makes sense to provide such environment values to the transformer * to be included into the key using <code>key</code> sitemap parameter.</p> * * @see CacheableProcessingComponent#getKey() */ public Serializable getKey() { /* * In case of including "cocoon://" or other dynamic sources key * ideally has to include ProcessingPipelineKey of the included * "cocoon://" sources, but it's not possible as at this time * we don't know yet which sources will get included into the * response. * * Hence, javadoc recommends providing key using sitemap parameter. */ return key == null? "I": "I" + key; } /** * <p>Generate (or return) the {@link SourceValidity} instance used to * possibly validate cached generations.</p> * * @return a <b>non null</b> {@link SourceValidity}. * @see org.apache.cocoon.caching.CacheableProcessingComponent#getValidity() */ public SourceValidity getValidity() { if (validity == null) { validity = new MultiSourceValidity(resolver, -1); } return validity; } /** * Description of the include element */ private class IncludeElement extends AbstractLogEnabled { /** Parameter controlling recursive includes processing */ private boolean recursive; /** Parameter controlling parallel (in multiple threads) includes processing */ private boolean parallel; /** Parameter controlling parallel (in multiple threads) includes processing in recursive includes */ private boolean recursiveParallel; /** The source base URI. */ private String base; /** The source URI to be included declared in an src attribute of the include element. */ protected String source; /** The flag indicating whether source content has to be parsed into XML or included as text. */ protected boolean parse; /** The mime type hint to the {@link org.apache.excalibur.xmlizer.XMLizer} when parsing the source content. */ protected String mimeType; /** The flag indicating whether to strip the root element. */ private boolean stripRoot; /** The buffer collecting fallback content. */ protected SaxBuffer fallback; /** A {@link Map} of the parameters to supply to the included source. */ protected Map parameters; /** The current parameter name captured. */ protected String parameter; /** The current parameter value (as a {@link StringBuffer}). */ protected StringBuffer value; /** Create include element */ private IncludeElement(String base, boolean parallel, boolean recursive, boolean recursiveParallel, Logger logger) { this.base = base; this.parallel = parallel; this.recursive = recursive; this.recursiveParallel = recursiveParallel; this.enableLogging(logger); } /** * Process element into the buffer. * This can not be shared buffer, as it must be cleaned if fallback is invoked. */ public void process(SaxBuffer buffer) throws SAXException { try { process0(buffer, buffer); } catch (SAXException e) { buffer.recycle(); if (this.fallback == null) { throw e; } if (getLogger().isInfoEnabled()) { getLogger().info("Failed to load <" + this.source + ">, using fallback.", e); } // Stream fallback through IncludeXMLPipe this.fallback.toSAX(new IncludeXMLPipe(getLogger(), buffer, buffer, recursive, recursiveParallel? parallel: false, recursiveParallel)); } } /** Load URI into the provided handlers, process fallback */ public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler) throws SAXException { try { if (this.fallback != null) { SaxBuffer buffer = new SaxBuffer(); process(buffer); buffer.toSAX(contentHandler); } else { process0(contentHandler, lexicalHandler); } } catch (SAXException e) { // source must not be cached if an error occurs validity = null; throw e; } } /** Load URI into the provided handlers. */ private void process0(ContentHandler contentHandler, LexicalHandler lexicalHandler) throws SAXException { Source source = null; if (getLogger().isDebugEnabled()) { getLogger().debug("Loading <" + this.source + ">"); } // Setup this thread's environment try { if (base != null) { source = resolver.resolveURI(this.source, base, null); } else { source = resolver.resolveURI(this.source); } if (validity != null) { synchronized (validity) { validity.addSource(source); } } // Include source if (this.parse && recursive) { SourceUtil.toSAX(manager, source, this.mimeType, new IncludeXMLPipe(getLogger(), contentHandler, lexicalHandler, recursive, recursiveParallel? parallel: false, recursiveParallel)); } else if (this.parse) { IncludeXMLConsumer includeXMLConsumer = new IncludeXMLConsumer(contentHandler, lexicalHandler); includeXMLConsumer.setIgnoreRootElement(stripRoot); SourceUtil.toSAX(manager, source, this.mimeType, includeXMLConsumer); } else { SourceUtil.toCharacters(source, "utf-8", contentHandler); } if (getLogger().isDebugEnabled()) { getLogger().debug("Loaded <" + this.source + ">"); } } catch (SAXException e) { if (getLogger().isDebugEnabled()) { getLogger().debug("Failed to load <" + this.source + ">", e); } throw e; } catch (ProcessingException e) { if (getLogger().isDebugEnabled()) { getLogger().debug("Failed to load <" + this.source + ">", e); } throw new SAXException(e); } catch (IOException e) { if (getLogger().isDebugEnabled()) { getLogger().debug("Failed to load <" + this.source + ">", e); } throw new SAXException(e); } finally { if (source != null) { resolver.release(source); } } } } /** * XML pipe reacting on the elements in the include namespace. */ private class IncludeXMLPipe extends AbstractXMLPipe { // // Configuration // /** Indicates whether this is root include pipe (owned by transformer) or a nested one */ private final boolean root; /** Parameter controlling recursive includes processing */ private boolean recursive; /** Parameter controlling parallel (in multiple threads) includes processing */ private boolean parallel; /** Parameter controlling parallel (in multiple threads) includes processing in recursive includes */ private boolean recursiveParallel; // // Current state // /** Stack of {@link XMLConsumer}s */ private final Stack consumers = new Stack(); /** Current depth of nested elements in the include namespace */ private int depth; /** Base URI used for the resolving included sources */ private String base; /** The source to be included declared in an include element. */ private IncludeElement element; /** If parallel processing is enabled, then this boolean tells us whether buffering has started yet. */ private boolean buffering; /** * <p>The IncludeBuffer that is used to buffering events if parallel * processing is turned on.</p> * <p>This object is also used as a lock for the thread counter <code>threads</code>.</p> */ private SaxBuffer buffer; /** Inclusion threads/tasks counter (if executing in parallel) */ private int threads; /** * <p>Create a new {@link IncludeXMLPipe} instance.</p> */ public IncludeXMLPipe() { root = true; } /** * <p>Create a new {@link IncludeXMLPipe} instance.</p> */ public IncludeXMLPipe(Logger logger, ContentHandler contentHandler, LexicalHandler lexicalHandler, boolean recursive, boolean parallel, boolean recursiveParallel) { root = false; this.enableLogging(logger); this.setContentHandler(contentHandler); this.setLexicalHandler(lexicalHandler); this.recursive = recursive; this.parallel = parallel; this.recursiveParallel = recursiveParallel; } /** * Finish processing. */ public void recycle() { if (this.buffering) { // Wait for threads to complete and release Sources waitForThreads(); this.buffering = false; this.buffer = null; } this.threads = 0; this.consumers.clear(); this.base = null; this.element = null; super.recycle(); } /** Push current consumer into the stack, replace with new one */ private void push(XMLConsumer consumer) { this.consumers.push(new Object[]{ super.xmlConsumer, super.contentHandler, super.lexicalHandler }); this.setConsumer(consumer); } /** Pop consumer from the stack, replace current one */ private void pop() { Object[] consumer = (Object[]) this.consumers.pop(); if (consumer[0] != null) { this.setConsumer((XMLConsumer) consumer[0]); } else { this.setContentHandler((ContentHandler) consumer[1]); this.setLexicalHandler((LexicalHandler) consumer[2]); } } // // ContentHandler interface // public void setDocumentLocator(Locator locator) { try { if (locator != null && locator.getSystemId() != null) { Source source = resolver.resolveURI(locator.getSystemId()); try { base = source.getURI(); } finally { resolver.release(source); } } } catch (IOException e) { getLogger().warn("Unable to resolve document base URI: <" + locator.getSystemId() + ">"); } super.setDocumentLocator(locator); } /** * <p>Receive notification of the beginning of an XML document.</p> * @see ContentHandler#startDocument */ public void startDocument() throws SAXException { if (root) { super.startDocument(); } } /** * <p>Receive notification of the end of an XML document.</p> * @see ContentHandler#startDocument */ public void endDocument() throws SAXException { /* This is the end of the line - process the buffered events */ if (this.buffering) { pop(); this.buffer.toSAX(super.contentHandler); } if (root) { super.endDocument(); } } /** * <p>Receive notification of the start of an element.</p> * @see ContentHandler#startElement */ public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { /* Check the namespace declaration */ if (NS_URI.equals(uri)) { /* * Depth 0: Outside of any include tag * Depth 1: Must be Inside <include> tag * Depth 2: Inside <fallback> tag */ depth++; /* Inclusion will not happen here but when we close this tag */ if (INCLUDE_ELEMENT.equals(localName) && depth == 1) { /* Check before we include (we don't want nested stuff) */ if (element != null) { throw new SAXException("Element " + INCLUDE_ELEMENT + " nested in another one."); } element = new IncludeElement(this.base, this.parallel, this.recursive, this.recursiveParallel, getLogger()); /* Remember the source we are trying to include */ element.source = atts.getValue(SRC_ATTRIBUTE); if (element.source == null || element.source.length() == 0) { throw new SAXException("Attribute '" + SRC_ATTRIBUTE + "' empty or missing."); } /* Defaults to 'xml' */ String value = atts.getValue(PARSE_ATTRIBUTE); if (value == null || value.equals("xml")) { element.parse = true; } else if (value.equals("text")) { element.parse = false; } else { throw new SAXException("Attribute '" + PARSE_ATTRIBUTE + "' has invalid value."); } /* Defaults to 'text/xml' */ element.mimeType = atts.getValue(MIME_ATTRIBUTE); if (!element.parse && element.mimeType != null) { throw new SAXException("Attribute '" + MIME_ATTRIBUTE + "' can't be specified for text inclusions."); } else if (element.mimeType == null) { element.mimeType = "text/xml"; } /* Defaults to false */ String stripRoot = atts.getValue(STRIP_ROOT_ATTRIBUTE); element.stripRoot = Boolean.valueOf(stripRoot).booleanValue(); /* Ignore nested content */ push(new NOPRecorder(){}); /* Done with this element */ return; } /* If this is a fallback parameter, capture its content. */ if (FALLBACK_ELEMENT.equals(localName) && depth == 2) { /* Check if we are in the right context */ if (element == null) { throw new SAXException("Element " + FALLBACK_ELEMENT + " specified outside of " + INCLUDE_ELEMENT + "."); } if (element.fallback != null) { throw new SAXException("Duplicate element " + FALLBACK_ELEMENT + "."); } /* Buffer fallback content */ push(element.fallback = new SaxBuffer()); /* Done with this element */ return; } /* If this is a parameter, then make sure we prepare. */ if (PARAMETER_ELEMENT.equals(localName) && depth == 2) { /* Check if we are in the right context */ if (element == null) { throw new SAXException("Element " + PARAMETER_ELEMENT + " specified outside of " + INCLUDE_ELEMENT + "."); } if (element.parameter != null) { throw new SAXException("Element " + PARAMETER_ELEMENT + " nested in another one."); } /* Get and process the parameter name */ element.parameter = atts.getValue(NAME_ATTRIBUTE); if (element.parameter == null || element.parameter.length() == 0) { throw new SAXException("Attribute '" + NAME_ATTRIBUTE + "' empty or missing."); } /* Make some room for the parameter value */ String value = atts.getValue(VALUE_ATTRIBUTE); if (value != null) { element.value = new StringBuffer(value); } /* Done with this element */ return; } /* We don't have a clue of why we got here (wrong element?) */ if (depth < 2) { throw new SAXException("Element '" + localName + "' was not expected here."); } } super.startElement(uri, localName, qName, atts); } /** * <p>Receive notification of the end of an element.</p> * @see ContentHandler#endElement */ public void endElement(String uri, String localName, String qName) throws SAXException { /* Check the namespace declaration */ if (NS_URI.equals(uri)) { /* * Depth 0: Outside of any include tag * Depth 1: Inside <include> tag * Depth 2: Inside <fallback> tag */ depth--; /* Inclusion will happen here, when we close the include element */ if (INCLUDE_ELEMENT.equals(localName) && depth == 0) { /* End ignoring nested content */ pop(); /* Get the source discovered opening the element and include */ if (element.parameters != null) { element.source = NetUtils.parameterize(element.source, element.parameters); element.parameters = null; } /* Check for parallel processing */ if (this.parallel) { if (!this.buffering) { this.buffering = true; buffer = new SaxBuffer(); push(buffer); } /* Process include element in separate thread */ buffer.xmlizable(new IncludeBuffer(element)); } else { /* Process include element inline */ element.process(super.contentHandler, super.lexicalHandler); } /* We are done with this include element */ this.element = null; return; } if (FALLBACK_ELEMENT.equals(localName) && depth == 1) { /* End buffering fallback content */ pop(); /* Done with this element */ return; } /* Addition of parameters happens here (so that we can capture chars) */ if (PARAMETER_ELEMENT.equals(localName) && depth == 1) { String value = (element.value != null? element.value.toString(): ""); /* Store the parameter name and value */ try { /* * Note: the parameter name and value are URL encoded, so that * weird characters such as "&" or "=" (have special meaning) * are passed through flawlessly. */ if (element.parameters == null) { element.parameters = new HashMap(5); } element.parameters.put(NetUtils.encode(element.parameter, ENCODING), NetUtils.encode(value, ENCODING)); } catch (UnsupportedEncodingException e) { throw new SAXException("Your platform does not support the " + ENCODING + " encoding", e); } /* We are done with this parameter element */ element.value = null; element.parameter = null; return; } } /* This is not our namespace, pass the event on! */ super.endElement(uri, localName, qName); } /** * <p>Receive notification of characters.</p> * @see ContentHandler#characters */ public void characters(char[] data, int offset, int length) throws SAXException { if (element != null && element.parameter != null) { /* If we have a parameter value to add to, let's add this chunk */ if (element.value == null) { element.value = new StringBuffer(); } element.value.append(data, offset, length); return; } /* Forward */ super.characters(data, offset, length); } // // Thread management // /** * Increment active threads counter */ int incrementThreads() { synchronized (buffer) { return ++threads; } } /** * Decrement active threads counter */ void decrementThreads() { synchronized (buffer) { if (--threads <= 0) { buffer.notify(); } } } /** * Wait till there is no active threads */ private void waitForThreads() { synchronized (buffer) { if (threads > 0) { if (getLogger().isDebugEnabled()) { getLogger().debug(threads + " threads in progress, waiting"); } try { buffer.wait(); } catch (InterruptedException e) { /* ignored */ } // Don't continue waiting if interrupted. } } } /** * Buffer for loading included source in separate thread. * Streaming of the loaded buffer possible only when source is * loaded completely. If loading is not complete, toSAX method * will block. */ private class IncludeBuffer extends SaxBuffer implements Runnable { private IncludeElement element; private int thread; private boolean finished; private SAXException e; public IncludeBuffer(IncludeElement element) { this.element = element; RunnableManager runnable = null; try { runnable = (RunnableManager) IncludeTransformer.this.manager.lookup(RunnableManager.ROLE); runnable.execute(IncludeTransformer.this.threadPool, this); } catch (final ServiceException e) { // In case we failed to spawn a thread throw new CascadingRuntimeException(e.getMessage(), e); } finally { IncludeTransformer.this.manager.release(runnable); } // Increment active threads counter this.thread = incrementThreads(); } /** * Load content of the source into this buffer. */ public void run() { try { if (getLogger().isDebugEnabled()) { getLogger().debug("Thread #" + thread + " loading <" + element.source + ">"); } // Setup this thread's environment CocoonComponentManager.enterEnvironment(environment, new WrapperComponentManager(manager), processor); try { element.process(this); } catch (SAXException e) { this.e = e; } finally { CocoonComponentManager.leaveEnvironment(); } } catch (RuntimeException e) { /* Unable to set thread's environment */ this.e = new SAXException(e); } finally { synchronized (this) { this.finished = true; notify(); } // Make sure that active threads counter is decremented decrementThreads(); } if (getLogger().isDebugEnabled()) { if (this.e == null) { getLogger().debug("Thread #" + thread + " loaded <" + element.source + ">"); } else { getLogger().debug("Thread #" + thread + " failed to load <" + element.source + ">", this.e); } } } /** * Stream content of this buffer when it is loaded completely. * This method blocks if loading is not complete. */ public void toSAX(ContentHandler contentHandler) throws SAXException { synchronized (this) { if (!this.finished) { try { wait(); } catch (InterruptedException e) { /* ignored */ } // Don't continue waiting if interrupted. } } if (this.e != null) { throw this.e; } super.toSAX(contentHandler); } } } }