cocoon-cvs mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From vgritse...@apache.org
Subject svn commit: r170036 - /cocoon/blocks/unsupported/scratchpad/trunk/java/org/apache/cocoon/transformation/IncludeTransformer.java
Date Fri, 13 May 2005 15:27:01 GMT
Author: vgritsenko
Date: Fri May 13 08:26:59 2005
New Revision: 170036

URL: http://svn.apache.org/viewcvs?rev=170036&view=rev
Log:
rewrite of the include transformer. new features:
 * included source URIs are relative to the included document
 * support recursive include processing (i.e., includes in included file)
   (configurable)
 * support fallback element processing with nested include elements
 * support parse="text" attribute
 * support mime type hint attribute
 * support cache key configuration parameter

Modified:
    cocoon/blocks/unsupported/scratchpad/trunk/java/org/apache/cocoon/transformation/IncludeTransformer.java

Modified: cocoon/blocks/unsupported/scratchpad/trunk/java/org/apache/cocoon/transformation/IncludeTransformer.java
URL: http://svn.apache.org/viewcvs/cocoon/blocks/unsupported/scratchpad/trunk/java/org/apache/cocoon/transformation/IncludeTransformer.java?rev=170036&r1=170035&r2=170036&view=diff
==============================================================================
--- cocoon/blocks/unsupported/scratchpad/trunk/java/org/apache/cocoon/transformation/IncludeTransformer.java (original)
+++ cocoon/blocks/unsupported/scratchpad/trunk/java/org/apache/cocoon/transformation/IncludeTransformer.java Fri May 13 08:26:59 2005
@@ -19,6 +19,7 @@
 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;
@@ -33,23 +34,28 @@
 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.EmbeddedXMLPipe;
+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
@@ -64,6 +70,20 @@
  *               src="cocoon://path/to/include"/&gt;
  * </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>
  *
@@ -74,7 +94,7 @@
  *
  * <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
+ * the <code>&amp;</code> (amperstand) or <code>=</code> (equals) character, which are
  * reserved characters in URIs. An example:</p>
  *
  * <pre>
@@ -85,11 +105,23 @@
  * &lt;/i:include&gt;
  * </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>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>
+ * &lt;i:include xmlns:i="http://apache.org/cocoon/include/1.0"
+ *               src="cocoon://path/to/include"&gt;
+ *   &lt;i:fallback&gt;
+ *     <strong>The data is temporarily unavailable.</strong>
+ *     We are sorry for the trouble; please try again later.
+ *   &lt;/i:fallback&gt;
+ * &lt;/i:include&gt;
+ * </pre>
+ *
  *
  * <h3>Parallel Processing</h3>
  * <p>Another feature of this {@link Transformer} is that it allows parallel processing
@@ -107,14 +139,33 @@
  * </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>
+ *   &lt;recursive&gt;true&lt;/recursive&gt;
+ * </pre>
+ *
+ * <p>or in a pipeline itself (to only affect that instance of the IncludeTransformer):</p>
+ * <pre>
+ *   &lt;map:parameter name="recursive" value="true"/&gt;
+ * </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.name include
  * @cocoon.sitemap.component.logger sitemap.transformer.include
- *
- * @cocoon.sitemap.component.pooling.max  16
+ * @cocoon.sitemap.component.pooling.max 16
+ * @version $Id$
  */
 public class IncludeTransformer extends AbstractTransformer
                                 implements Serviceable, Configurable,
@@ -126,12 +177,21 @@
     /** <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";
 
@@ -145,83 +205,66 @@
     // Global configuration
     //
 
-    /** <p>The {@link ServiceManager} instance associated with this instance.</p> */
+    /** The {@link ServiceManager} instance associated with this instance. */
     private ServiceManager manager;
 
-    /**
-     * <p>Configuration option controlling parallel (in multiple threads)
-     * includes processing</p>
-     */
+    /** 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
     //
 
-    /** <p>The {@link SourceResolver} used to resolve included URIs.</p> */
+    /** The {@link SourceResolver} used to resolve included URIs. */
     private SourceResolver resolver;
 
-    /**
-     * <p>Pipeline parameter controlling parallel (in multiple threads)
-     * includes processing</p>
-     */
-    private boolean parallel;
+    /** 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
     //
 
-    /** <p>The {@link SourceValidity} instance associated with this request.</p> */
+    /** The {@link SourceValidity} instance associated with this request. */
     private MultiSourceValidity validity;
 
-    /** <p>A {@link NamespacesTable} used to filter namespace declarations.</p> */
+    /** A {@link NamespacesTable} used to filter namespace declarations. */
     private NamespacesTable namespaces;
 
-    /** <p>A {@link Map} of the parameters to supply to the included source.</p> */
-    private Map parameters;
-
-    /** <p>The source to be included declared in an include element.</p> */
-    private String source;
-
-    /** <p>The current parameter name captured.</p> */
-    private String parameter;
-
-    /** <p>The current parameter value (as a {@link StringBuffer}).</p> */
-    private StringBuffer value;
-
-    /**
-     * <p>If parallel processing is enabled, then this boolean tells us
-     * whether buffering has started yet.</p>
-     */
-    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 thread counter m_threads</p>
-     */
-    private SaxBuffer buffer;
-
-    /**
-     * <p>Inclusion threads/tasks counter (if executing in parallel)</p>
-     */
-    private int threads;
-
-    /**
-     * <p>Inclusion threads/tasks environment (if executing in parallel)</p>
-     */
-    private Environment environment;
+    /** The {@link IncludeXMLPipe} which is doing all the work */
+    private final IncludeXMLPipe pipe;
 
     /**
-     * <p>Inclusion threads/tasks processor (if executing in parallel)</p>
+     * <p>Create a new {@link IncludeTransformer} instance.</p>
      */
-    private Processor processor;
-
+    public IncludeTransformer() {
+        pipe = new IncludeXMLPipe();
+    }
 
     /**
-     * <p>Create a new {@link IncludeTransformer} instance.</p>
+     * <p>Initialize own and {@link #pipe} loggers</p>
      */
-    public IncludeTransformer() {
+    public void enableLogging(Logger logger) {
+        super.enableLogging(logger);
+        pipe.enableLogging(logger);
     }
 
     /**
@@ -237,8 +280,13 @@
      * @see Configurable#configure(Configuration)
      */
     public void configure(Configuration configuration) throws ConfigurationException {
-        // Get value of parallel node from the configuration - defaults to false
+        /* 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);
     }
 
     /**
@@ -249,21 +297,38 @@
      */
     public void setup(SourceResolver resolver, Map om, String src, Parameters parameters)
     throws ProcessingException, SAXException, IOException {
-        // Read parameters
-        this.parallel = parameters.getParameterAsBoolean("parallel", this.defaultParallel);
+        /* 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);
 
-        if (this.parallel) {
+        /* Init transformer state */
+        if (this.pipe.parallel) {
             this.environment = EnvironmentHelper.getCurrentEnvironment();
             this.processor = EnvironmentHelper.getCurrentProcessor();
         }
-
-        // Init transformer state
         this.namespaces = new NamespacesTable();
         this.resolver = resolver;
         this.validity = null;
-        this.parameters = null;
-        this.value = null;
-        this.buffering = false;
+
+        // 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);
     }
 
     /**
@@ -274,16 +339,9 @@
     public void recycle() {
         this.namespaces = null;
         this.validity = null;
-        this.parameters = null;
-        this.value = null;
-        this.source = null;
-
-        if (this.buffering) {
-            // Wait for threads to complete and release Sources
-            waitForThreads();
-            this.buffering = false;
-            this.buffer = 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.
@@ -292,10 +350,11 @@
         super.recycle();
     }
 
+
     /**
      * <p>Receive notification of the beginning of an XML document.</p>
      *
-     * @see org.xml.sax.ContentHandler#startDocument()
+     * @see ContentHandler#startDocument
      */
     public void startDocument()
     throws SAXException {
@@ -308,18 +367,13 @@
     /**
      * <p>Receive notification of the end of an XML document.</p>
      *
-     * @see org.xml.sax.ContentHandler#startDocument()
+     * @see ContentHandler#startDocument()
      */
     public void endDocument()
     throws SAXException {
         /* Make sure that the validity is "closed" at the end */
         this.validity.close();
 
-        /* This is the end of the line - process the buffered events */
-        if (this.buffering) {
-            this.buffer.toSAX(super.contentHandler);
-        }
-
         super.endDocument();
     }
 
@@ -339,11 +393,7 @@
             this.namespaces.addDeclaration(prefix, nsuri);
         } else {
             /* Map the current prefix, as we don't know it */
-            if (this.buffering) {
-                buffer.startPrefixMapping(prefix, nsuri);
-            } else {
-                super.startPrefixMapping(prefix, nsuri);
-            }
+            super.startPrefixMapping(prefix, nsuri);
         }
     }
 
@@ -363,367 +413,705 @@
             this.namespaces.removeDeclaration(prefix);
         } else {
             /* Unmap the current prefix, as we don't know it */
-            if (this.buffering) {
-                buffer.endPrefixMapping(prefix);
-            } else {
-                super.endPrefixMapping(prefix);
-            }
+            super.endPrefixMapping(prefix);
         }
     }
 
     /**
-     * <p>Receive notification of characters.</p>
+     * <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 org.xml.sax.ContentHandler#characters(char[], int, int)
+     * @see CacheableProcessingComponent#getKey()
      */
-    public void characters(char data[], int offset, int length)
-    throws SAXException {
-        /* If we have a parameter value to add to, let's add this chunk */
-        if (this.parameter != null) {
-            if (this.value == null) {
-                this.value = new StringBuffer();
-            }
-            this.value.append(data, offset, length);
+    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;
+    }
 
-        /* Forward this only if we are not inside an include tag */
-        } else if (this.source == null) {
-            if (this.buffering) {
-                buffer.characters(data, offset, length);
-            } else {
-                super.characters(data, offset, length);
-            }
+    /**
+     * <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;
     }
 
     /**
-     * <p>Receive notification of the start of an element.</p>
-     *
-     * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes)
+     * Description of the include element
      */
-    public void startElement(String uri, String localName, String qName, Attributes atts)
-    throws SAXException {
-        /* Check the namespace declaration */
-        if (NS_URI.equals(uri)) {
-            /* Inclusion will not happen here but when we close this tag */
-            if (INCLUDE_ELEMENT.equals(localName)) {
-                /* Check before we include (we don't want nested stuff) */
-                if (this.source != null) {
-                    throw new SAXException("Invalid include nested in another");
-                }
-
-                /* Remember the source we are trying to include */
-                this.source = atts.getValue(SRC_ATTRIBUTE);
-                if ((this.source == null) || (this.source.length() == 0)) {
-                    throw new SAXException("Attribute \"" + SRC_ATTRIBUTE
-                                           + "\" not specified");
-                }
-
-                /* Whatever list of parameters we got before, we wipe it! */
-                this.parameters = null;
-                this.value = null;
-                this.parameter = null;
+    private class IncludeElement {
+        /** Parameter controlling recursive includes processing */
+        private boolean recursive;
 
-                /* Done with this element */
-                return;
+        /** 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 + ">");
             }
 
-            /* If this is a parameter, then make sure we prepare. */
-            if (PARAMETER_ELEMENT.equals(localName)) {
-                /* Check if we are in the right context */
-                if (this.source == null) {
-                    throw new SAXException("Parameter specified outside of include");
+            // 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);
+                    }
                 }
-                if (this.parameter != null) {
-                    throw new SAXException("Invalid parameter nested in another");
+
+                // 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);
                 }
 
-                /* Get and process the parameter name */
-                this.parameter = atts.getValue(NAME_ATTRIBUTE);
-                if ((this.parameter == null) || (this.parameter.length() == 0)) {
-                    throw new SAXException("Attribute \"" + NAME_ATTRIBUTE
-                                           + "\" not specified");
+                if (getLogger().isDebugEnabled()) {
+                    getLogger().debug("Loaded <" + this.source + ">");
+                }
+            } catch (SAXException e) {
+                if (getLogger().isDebugEnabled()) {
+                    getLogger().debug("Failed to load <" + this.source + ">", e);
                 }
 
-                /* Make some room for the parameter value */
-                String value = atts.getValue(VALUE_ATTRIBUTE);
-                if (value != null) this.value = new StringBuffer(value);
+                throw e;
 
-                /* Done with this element */
-                return;
-            }
+            } 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);
+                }
 
-            /* We don't have a clue of why we got here (wrong element?) */
-            if (getLogger().isWarnEnabled()) {
-                getLogger().warn("Unknown element \"" + localName + "\"");
+                throw new SAXException(e);
+
+            } finally {
+                if (source != null) {
+                    resolver.release(source);
+                }
             }
-            return;
+        }
+    }
+
+    /**
+     * 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;
         }
 
-        /* Not our namespace, simply check and pass this element on! */
-        if (this.source == null) {
+        /**
+         * Finish processing.
+         */
+        public void recycle() {
             if (this.buffering) {
-                buffer.startElement(uri, localName, qName, atts);
+                // 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 {
-                super.startElement(uri, localName, qName, atts);
+                setContentHandler((ContentHandler) consumer[1]);
+                setLexicalHandler((LexicalHandler) consumer[2]);
             }
-            return;
         }
-        throw new SAXException("Element <" + qName + "/> invalid inside include");
-    }
 
-    /**
-     * <p>Receive notification of the end of an element.</p>
-     *
-     * @see org.xml.sax.ContentHandler#endElement(String, String, String)
-     */
-    public void endElement(String uri, String localName, String qName)
-    throws SAXException {
-        /* Check the namespace declaration */
-        if (NS_URI.equals(uri)) {
+        //
+        // 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() + ">");
+            }
 
-            /* Inclusion will happen here, when we close the include element */
-            if (INCLUDE_ELEMENT.equals(localName)) {
+            super.setDocumentLocator(locator);
+        }
 
-                /* Get the source discovered opening the element and include */
-                Source source = null;
-                try {
-                    if (this.parameters != null) {
-                        this.source = NetUtils.parameterize(this.source,
-                                                              this.parameters);
+        /**
+         * <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.");
                     }
-                    source = this.resolver.resolveURI(this.source);
-                    if (this.validity != null) this.validity.addSource(source);
+                    element = new IncludeElement(this.base, this.parallel, this.recursive, this.recursiveParallel);
 
-                    /* Check for parallel processing */
-                    if (this.parallel) {
-                        this.buffering = true;
-                        if (buffer == null) {
-                            buffer = new SaxBuffer();
-                        }
-                        buffer.xmlizable(new IncludeBuffer(source));
+                    /* 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 {
-                        SourceUtil.toSAX(this.manager, source, "text/xml",
-                                         new IncludeXMLConsumer(super.contentHandler));
+                        throw new SAXException("Attribute '" + PARSE_ATTRIBUTE + "' has invalid value.");
                     }
-                } catch (IOException e) {
-                    /* Something bad happenend processing a stream */
-                    throw new SAXException(e);
-                } catch (ProcessingException e) {
-                    /* Something bad happened processing a pipeline */
-                    throw new SAXException(e);
-                } finally {
-                    /* Make sure we release the source if we aren't in parellel mode.
-                       In parallel mode, the spawned thread releases the source afer processing */
-                    if (!this.buffering && source != null) {
-                        this.resolver.release(source);
+
+                    /* 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;
                 }
 
-                /* We are done with the include element */
-                this.parameters = null;
-                this.value = null;
-                this.parameter = null;
-                this.source = null;
-                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 + ".");
+                    }
 
-            /* Addition of parameters happens here (so that we can capture chars) */
-            if (PARAMETER_ELEMENT.equals(localName)) {
-                String value = (this.value != null? this.value.toString(): "");
+                    /* Buffer fallback content */
+                    push(element.fallback = new SaxBuffer());
 
-                /* 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 (this.parameters == null) this.parameters = new HashMap(5);
-                    this.parameters.put(NetUtils.encode(this.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 */
-                this.value = null;
-                this.parameter = null;
-                return;
-            }
+                    /* Done with this element */
+                    return;
+                }
 
-        } else {
-            /* This is not our namespace, pass the event on! */
-            if (this.buffering) {
-                buffer.endElement(uri, localName, qName);
-            } else {
-                super.endElement(uri, localName, qName);
+                /* 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>Return the validity key associated with this transformation.</p>
-     *
-     * @see CacheableProcessingComponent#getKey()
-     */
-    public Serializable getKey() {
-        /*
-         * FIXME: In case of including "cocoon://" or other dynamic sources
-         * key has to be dynamic.
+        /**
+         * <p>Receive notification of the end of an element.</p>
+         * @see ContentHandler#endElement
          */
-        return "I";
-    }
+        public void endElement(String uri, String localName, String qName)
+        throws SAXException {
+            /* Check the namespace declaration */
+            if (NS_URI.equals(uri)) {
 
-    /**
-     * <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;
-    }
+                /*
+                 * 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;
+                    }
 
-    /**
-     * Increment active threads counter
-     */
-    int incrementThreads() {
-        synchronized (buffer) {
-            return ++threads;
-        }
-    }
+                    /* Check for parallel processing */
+                    if (this.parallel) {
+                        if (!this.buffering) {
+                            this.buffering = true;
+                            buffer = new SaxBuffer();
+                            push(buffer);
+                        }
 
-    /**
-     * Decrement active threads counter
-     */
-    void decrementThreads() {
-        synchronized (buffer) {
-            if (--threads <= 0) {
-                buffer.notify();
+                        /* 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);
         }
-    }
 
-    /**
-     * 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");
+        /**
+         * <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();
                 }
-
-                try {
-                    buffer.wait();
-                } catch (InterruptedException ignored) { }
-                // Don't continue waiting if interrupted.
+                element.value.append(data, offset, length);
+                return;
             }
+
+            /* Forward */
+            super.characters(data, offset, length);
         }
-    }
 
-    /**
-     * 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 Source source;
-        private boolean finished;
-        private SAXException e;
+        //
+        // Thread management
+        //
 
-        public IncludeBuffer(Source source) {
-            this.source = source;
+        /**
+         * Increment active threads counter
+         */
+        int incrementThreads() {
+            synchronized (buffer) {
+                return ++threads;
+            }
+        }
 
-            try {
-                final RunnableManager runnableManager = (RunnableManager) manager.lookup(RunnableManager.ROLE);
-                runnableManager.execute("daemon", this); // XXX: GP: Do we really need daemon threads here ?
-                manager.release(runnableManager);
-            } catch (final ServiceException e) {
-                // In case we failed to spawn a thread
-                this.e = new SAXException(e);
-                resolver.release(source);
-                throw new CascadingRuntimeException(e.getMessage(), e);
-            } catch (RuntimeException e) {
-                // In case we failed to spawn a thread
-                this.e = new SAXException(e);
-                resolver.release(source);
-                throw e;
+        /**
+         * Decrement active threads counter
+         */
+        void decrementThreads() {
+            synchronized (buffer) {
+                if (--threads <= 0) {
+                    buffer.notify();
+                }
             }
         }
 
         /**
-         * Stream content of this buffer when it is loaded completely.
-         * This method blocks if loading is not complete.
+         * Wait till there is no active threads
          */
-        public void toSAX(ContentHandler contentHandler)
-        throws SAXException {
-            synchronized (this) {
-                if (!this.finished) {
+        private void waitForThreads() {
+            synchronized (buffer) {
+                if (threads > 0) {
+                    if (getLogger().isDebugEnabled()) {
+                        getLogger().debug(threads + " threads in progress, waiting");
+                    }
+
                     try {
-                        wait();
+                        buffer.wait();
                     } catch (InterruptedException ignored) { }
                     // Don't continue waiting if interrupted.
                 }
             }
-
-            if (this.e != null) {
-                throw this.e;
-            }
-
-            super.toSAX(contentHandler);
         }
 
         /**
-         * Load content of the source into this buffer.
+         * 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.
          */
-        public void run() {
-            // Increment active threads counter
-            int t = incrementThreads();
-            try {
-                if (getLogger().isDebugEnabled()) {
-                    getLogger().debug("Thread #" + t + " loading <" + source.getURI() + ">");
+        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);
                 }
 
-                // Setup this thread's environment
-                EnvironmentHelper.enterProcessor(processor, manager, environment);
+                // Increment active threads counter
+                this.thread = incrementThreads();
+            }
+
+            /**
+             * Load content of the source into this buffer.
+             */
+            public void run() {
                 try {
-                    // Include source
-                    SourceUtil.toSAX(manager, this.source, "text/xml", new EmbeddedXMLPipe(this));
-                } catch (Exception e) {
-                    if (!(e instanceof SAXException)) {
-                        this.e = new SAXException(e);
-                    } else {
-                        this.e = (SAXException) e;
+                    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 {
-                    resolver.release(this.source);
-                    EnvironmentHelper.leaveProcessor();
-                }
-            } catch (ProcessingException e) {
-                this.e = new SAXException(e);
-            } finally {
-                synchronized (this) {
-                    this.finished = true;
-                    notify();
+                    synchronized (this) {
+                        this.finished = true;
+                        notify();
+                    }
+
+                    // Make sure that active threads counter is decremented
+                    decrementThreads();
                 }
 
-                // 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);
+                    }
+                }
             }
 
-            if (getLogger().isDebugEnabled()) {
-                if (this.e == null) {
-                    getLogger().debug("Thread #" + t + " loaded <" + source.getURI() + ">");
-                } else {
-                    getLogger().debug("Thread #" + t + " failed to load <" + source.getURI() + ">", 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);
             }
         }
     }



Mime
View raw message