| /* |
| * Copyright 1999-2005 The Apache Software Foundation. |
| * |
| * Licensed 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.configuration.Configurable; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| 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.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.environment.internal.EnvironmentHelper; |
| 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>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, |
| Transformer, 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 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. */ |
| private 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). */ |
| private 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. */ |
| private SourceResolver resolver; |
| |
| /** The {@link Environment} used within parallel threads */ |
| private 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. */ |
| private 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 = EnvironmentHelper.getCurrentEnvironment(); |
| this.processor = EnvironmentHelper.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 { |
| /** 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. */ |
| private String source; |
| |
| /** The flag indicating whether source content has to be parsed into XML or included as text. */ |
| private boolean parse; |
| |
| /** The mime type hint to the {@link org.apache.excalibur.xmlizer.XMLizer} when parsing the source content. */ |
| private String mimeType; |
| |
| /** The buffer collecting fallback content. */ |
| private SaxBuffer fallback; |
| |
| /** A {@link Map} of the parameters to supply to the included source. */ |
| private Map parameters; |
| |
| /** The current parameter name captured. */ |
| private String parameter; |
| |
| /** The current parameter value (as a {@link StringBuffer}). */ |
| private StringBuffer value; |
| |
| /** Create include element */ |
| private IncludeElement(String base, boolean parallel, boolean recursive, boolean recursiveParallel) { |
| this.base = base; |
| this.parallel = parallel; |
| this.recursive = recursive; |
| this.recursiveParallel = recursiveParallel; |
| } |
| |
| /** |
| * 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 { |
| if (this.fallback != null) { |
| SaxBuffer buffer = new SaxBuffer(); |
| process(buffer); |
| buffer.toSAX(contentHandler); |
| } else { |
| process0(contentHandler, lexicalHandler); |
| } |
| } |
| |
| /** 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) { |
| SourceUtil.toSAX(manager, source, this.mimeType, |
| new IncludeXMLConsumer(contentHandler, lexicalHandler)); |
| } 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; |
| enableLogging(logger); |
| setContentHandler(contentHandler); |
| 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 }); |
| setConsumer(consumer); |
| } |
| |
| /** Pop consumer from the stack, replace current one */ |
| private void pop() { |
| Object[] consumer = (Object[]) this.consumers.pop(); |
| if (consumer[0] != null) { |
| setConsumer((XMLConsumer) consumer[0]); |
| } else { |
| setContentHandler((ContentHandler) consumer[1]); |
| 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); |
| |
| /* 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"; |
| } |
| |
| /* 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 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 { |
| Source source = null; |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Thread #" + thread + " loading <" + element.source + ">"); |
| } |
| |
| // Setup this thread's environment |
| EnvironmentHelper.enterProcessor(processor, manager, environment); |
| try { |
| element.process(this); |
| |
| } catch (SAXException e) { |
| this.e = e; |
| |
| } finally { |
| if (source != null) { |
| resolver.release(source); |
| } |
| EnvironmentHelper.leaveProcessor(); |
| } |
| } catch (ProcessingException 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 ignored) { } |
| // Don't continue waiting if interrupted. |
| } |
| } |
| |
| if (this.e != null) { |
| throw this.e; |
| } |
| |
| super.toSAX(contentHandler); |
| } |
| } |
| } |
| } |