You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@cocoon.apache.org by gi...@apache.org on 2003/07/10 10:12:49 UTC

cvs commit: cocoon-2.1/src/scratchpad/src/org/apache/cocoon/generation TraversableGenerator.java XPathTraversableGenerator.java

gianugo     2003/07/10 01:12:49

  Added:       src/scratchpad/src/org/apache/cocoon/generation
                        TraversableGenerator.java
                        XPathTraversableGenerator.java
  Log:
  A Source-oriented refactoring of DirectoryGenerator (and its companion
  XPathDirectoryGenerator). It should work now with any TraversableSource,
  making it possible to generate (and query with XPath) collection listings
  from more than just files and directories.
  
  XPath functions have been revamped too: it's now possible to add namespaces
  to queries
  
  Revision  Changes    Path
  1.1                  cocoon-2.1/src/scratchpad/src/org/apache/cocoon/generation/TraversableGenerator.java
  
  Index: TraversableGenerator.java
  ===================================================================
  /*
  
   ============================================================================
                     The Apache Software License, Version 1.1
   ============================================================================
  
   Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.
  
   Redistribution and use in source and binary forms, with or without modifica-
   tion, are permitted provided that the following conditions are met:
  
   1. Redistributions of  source code must  retain the above copyright  notice,
      this list of conditions and the following disclaimer.
  
   2. Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
  
   3. The end-user documentation included with the redistribution, if any, must
      include  the following  acknowledgment:  "This product includes  software
      developed  by the  Apache Software Foundation  (http://www.apache.org/)."
      Alternately, this  acknowledgment may  appear in the software itself,  if
      and wherever such third-party acknowledgments normally appear.
  
   4. The names "Apache Cocoon" and  "Apache Software Foundation" must  not  be
      used to  endorse or promote  products derived from  this software without
      prior written permission. For written permission, please contact
      apache@apache.org.
  
   5. Products  derived from this software may not  be called "Apache", nor may
      "Apache" appear  in their name,  without prior written permission  of the
      Apache Software Foundation.
  
   THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
   FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
   APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
   INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
   DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
   OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
   ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
   (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
   This software  consists of voluntary contributions made  by many individuals
   on  behalf of the Apache Software  Foundation and was  originally created by
   Stefano Mazzocchi  <st...@apache.org>. For more  information on the Apache
   Software Foundation, please see <http://www.apache.org/>.
  
  */
  package org.apache.cocoon.generation;
  
  import org.apache.avalon.framework.parameters.Parameters;
  import org.apache.cocoon.ProcessingException;
  import org.apache.cocoon.ResourceNotFoundException;
  import org.apache.cocoon.caching.CacheableProcessingComponent;
  import org.apache.cocoon.components.source.SourceUtil;
  import org.apache.cocoon.environment.SourceResolver;
  import org.apache.excalibur.source.Source;
  import org.apache.excalibur.source.SourceException;
  import org.apache.excalibur.source.SourceValidity;
  import org.apache.excalibur.source.TraversableSource;
  import org.apache.regexp.RE;
  import org.apache.regexp.RESyntaxException;
  import org.xml.sax.SAXException;
  import org.xml.sax.helpers.AttributesImpl;
  
  import java.io.IOException;
  import java.io.Serializable;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Date;
  import java.util.List;
  import java.util.Map;
  import java.util.Stack;
  import java.util.Arrays;
  import java.util.Comparator;
  
  /**
   * Generates an XML directory listing from a Traversable Source. This class
   * is nothing more than a Source-oriented refactoring of DirectoryGenerator.
   * <p>
   * The root node of the generated document will normally be a
   * <code>directory</code> node, and a directory node can contain zero
   * or more <code>file</code> or directory nodes. A file node has no
   * children. Each node will contain the following attributes:
   * <blockquote>
   *   <dl>
   *   <dt> name
   *   <dd> the name of the file or directory
   *   <dt> lastModified
   *   <dd> the time the file was last modified, measured as the number of
   *   milliseconds since the epoch (as in java.io.File.lastModified)
   *   <dt> size
   *   <dd> the file size, in bytes (as in java.io.File.length)
   *   <dt> date (optional)
   *   <dd> the time the file was last modified in human-readable form
   *   </dl>
   * </blockquote>
   * <p>
   * <b>Configuration options:</b>
   * <dl>
   * <dt> <i>depth</i> (optional)
   * <dd> Sets how deep DirectoryGenerator should delve into the
   * directory structure. If set to 1 (the default), only the starting
   * directory's immediate contents will be returned.
   * <dt> <i>sort</i> (optional)
   * <dd> Sort order in which the nodes are returned. Possible values are
   * name, size, time, collection. collection is the same as name,
   * except that the directory entries are listed first. System order is default.
   * <dt> <i>reverse</i> (optional)
   * <dd>	Reverse the order of the sort
   * <dt> <i>dateFormat</i> (optional)
   * <dd> Sets the format for the date attribute of each node, as
   * described in java.text.SimpleDateFormat. If unset, the default
   * format for the current locale will be used.
   * <dt> <i>refreshDelay</i> (optional)
   * <dd> Sets the delay (in seconds) between checks on the filesystem for changed content.
   * Defaults to 1 second.
   * </dl>
   *
   * @author <a href="mailto:pier@apache.org">Pierpaolo Fumagalli</a>
   *         (Apache Software Foundation)
   * @author <a href="mailto:conny@smb-tec.com">Conny Krappatsch</a>
   *         (SMB GmbH) for Virbus AG
   * @author <a href="d.madama@pro-netics.com">Daniele Madama</a>
   * @author <a href="gianugo@apache.org">Gianugo Rabellino</a>
   * 
   * @version CVS $Id: TraversableGenerator.java,v 1.1 2003/07/10 08:12:49 gianugo Exp $
   */
  public class TraversableGenerator extends ComposerGenerator implements CacheableProcessingComponent {
  
      /** The URI of the namespace of this generator. */
      protected static final String URI = "http://apache.org/cocoon/collection/1.0";
  
      /** The namespace prefix for this namespace. */
      protected static final String PREFIX = "collection";
  
      /* Node and attribute names */
      protected static final String COL_NODE_NAME = "collection";
      protected static final String RESOURCE_NODE_NAME = "file";
  
      protected static final String RES_NAME_ATTR_NAME = "name";
      protected static final String LASTMOD_ATTR_NAME = "lastModified";
      protected static final String DATE_ATTR_NAME = "date";
      protected static final String SIZE_ATTR_NAME = "size";
  
      /** The validity that is being built */
      protected CollectionValidity validity;
      /** Convenience object, so we don't need to create an AttributesImpl for every element. */
      protected AttributesImpl attributes;
  
      /** The depth parameter determines how deep the DirectoryGenerator should delve. */
      protected int depth;
      /**
       * The dateFormatter determines into which date format the lastModified
       * time should be converted.
       * FIXME: SimpleDateFormat is not supported by all locales!
       */
      protected SimpleDateFormat dateFormatter;
      /** The delay between checks on updates to the filesystem. */
      protected long refreshDelay;
      /**
       * The sort parameter determines by which attribute the content of one
       * directory should be sorted. Possible values are "name", "size", "time"
       * and "directory", where "directory" is the same as "name", except that
       * directory entries are listed first.
       */
      protected String sort;
      /** The reverse parameter reverses the sort order. <code>false</code> is default. */
      protected boolean reverse;
      /** The regular expression for the root pattern. */
      protected RE rootRE;
      /** The regular expression for the include pattern. */
      protected RE includeRE;
      /** The regular expression for the exclude pattern. */
      protected RE excludeRE;
      /**
       * This is only set to true for the requested directory specified by the
       * <code>src</code> attribute on the generator's configuration.
       */
      protected boolean isRequestedCollection;
  
      /**
       * Set the request parameters. Must be called before the generate method.
       *
       * @param resolver     the SourceResolver object
       * @param objectModel  a <code>Map</code> containing model object
       * @param src          the directory to be XMLized specified as src attribute on &lt;map:generate/>
       * @param par          configuration parameters
       */
      public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
              throws ProcessingException, SAXException, IOException {
          if (src == null) {
              throw new ProcessingException("No src attribute pointing to a traversable source to be XMLized specified.");
          }
          super.setup(resolver, objectModel, src, par);
  
          this.depth = par.getParameterAsInteger("depth", 1);
  
          String dateFormatString = par.getParameter("dateFormat", null);
          if (dateFormatString != null) {
              this.dateFormatter = new SimpleDateFormat(dateFormatString);
          } else {
              this.dateFormatter = new SimpleDateFormat();
          }
  
          this.sort = par.getParameter("sort", "name");
  
          this.reverse = par.getParameterAsBoolean("reverse", false);
  
          this.refreshDelay = par.getParameterAsLong("refreshDelay", 1L) * 1000L;
  
          if (this.getLogger().isDebugEnabled()) {
              this.getLogger().debug("depth: " + this.depth);
              this.getLogger().debug("dateFormat: " + this.dateFormatter.toPattern());
              this.getLogger().debug("sort: " + this.sort);
              this.getLogger().debug("reverse: " + this.reverse);
              this.getLogger().debug("refreshDelay: " + this.refreshDelay);
          }
  
          String rePattern = null;
          try {
              rePattern = par.getParameter("root", null);
              this.rootRE = (rePattern == null) ? null : new RE(rePattern);
              if (this.getLogger().isDebugEnabled()) {
                  this.getLogger().debug("root pattern: " + rePattern);
              }
  
              rePattern = par.getParameter("include", null);
              this.includeRE = (rePattern == null) ? null : new RE(rePattern);
              if (this.getLogger().isDebugEnabled()) {
                  this.getLogger().debug("include pattern: " + rePattern);
              }
  
              rePattern = par.getParameter("exclude", null);
              this.excludeRE = (rePattern == null) ? null : new RE(rePattern);
  
              if (this.getLogger().isDebugEnabled()) {
                  this.getLogger().debug("exclude pattern: " + rePattern);
              }
          } catch (RESyntaxException rese) {
              throw new ProcessingException("Syntax error in regexp pattern '"
                                            + rePattern + "'", rese);
          }
  
          this.isRequestedCollection = false;
          this.attributes = new AttributesImpl();
      }
  
      /** 
       * @see org.apache.cocoon.caching.CacheableProcessingComponent#getKey()
       * FIXME: SimpleDateFormat and RE don't have a toString() implemented, so
       *        the key generation is buggy!!
       */
      public Serializable getKey() {
          return super.source + this.depth + this.dateFormatter + this.sort
                 + this.reverse + this.rootRE + this.excludeRE + this.includeRE;
      }
  
      /**
       * Gets the source validity, using a deferred validity object. The validity
       * is initially empty since the files that define it are not known before
       * generation has occured. So the returned object is kept by the generator
       * and filled with each of the files that are traversed.
       * @see DirectoryGenerator.DirValidity
       */
      public SourceValidity getValidity() {
          this.validity = new CollectionValidity(this.refreshDelay);
          return this.validity;
      }
  
      /**
       * Generate XML data.
       *
       * @throws  SAXException
       *      if an error occurs while outputting the document
       * @throws  ProcessingException
       *      if the requsted URI isn't a directory on the local filesystem
       */
      public void generate() throws SAXException, ProcessingException {
          String directory = super.source;
          TraversableSource inputSource = null;
          try {
              inputSource = (TraversableSource)this.resolver.resolveURI(directory);
              String systemId = inputSource.getURI();
                          
              if (!inputSource.isCollection()) {
                  throw new ResourceNotFoundException(directory + " is not a collection.");
              }
  
              this.contentHandler.startDocument();
  
              Stack ancestors = getAncestors(inputSource);
              addAncestorPath(inputSource, ancestors);
  
              this.contentHandler.endDocument();
          } catch (SourceException se) {
              throw SourceUtil.handle(se);
          } catch (IOException ioe) {
              throw new ResourceNotFoundException("Could not read collection " + directory, ioe);
          } catch (ClassCastException ce) {
              throw new ResourceNotFoundException(directory + 
                  " is not a traversable source");
  
          } finally {
              this.resolver.release(inputSource);
          }
      }
  
      /**
       * Creates a stack containing the ancestors of File up to specified directory.
       * @param path the File whose ancestors shall be retrieved
       * @return a Stack containing the ancestors.
       */
      protected Stack getAncestors(TraversableSource path) throws IOException {
          TraversableSource parent = path;
          Stack ancestors = new Stack();
  
          while ((parent != null) && !isRoot(parent)) {
              parent = (TraversableSource)parent.getParent();
   
              if (parent != null) {
                  ancestors.push(parent);
              } else {
                  // no ancestor matched the root pattern
                  ancestors.clear();
              }
          }
  
          return ancestors;
      }
  
      /**
       * Adds recursively the path from the directory matched by the root pattern
       * down to the requested directory.
       * @param path       the requested directory.
       * @param ancestors  the stack of the ancestors.
       * @throws SAXException
       */
      protected void addAncestorPath(TraversableSource path, Stack ancestors) throws SAXException {
          if (ancestors.empty()) {
              this.isRequestedCollection = true;
              addPath(path, depth);
          } else {
              startNode(COL_NODE_NAME, (TraversableSource)ancestors.pop());
              addAncestorPath(path, ancestors);
              endNode(COL_NODE_NAME);
          }
      }
  
      /**
       * Adds a single node to the generated document. If the path is a
       * directory, and depth is greater than zero, then recursive calls
       * are made to add nodes for the directory's children.
       *
       * @param path   the file/directory to process
       * @param depth  how deep to scan the directory
       *
       * @throws SAXException  if an error occurs while constructing nodes
       */
      protected void addPath(TraversableSource path, int depth) throws SAXException {
          if (path.isCollection()) {
              startNode(COL_NODE_NAME, path);
              if (depth > 0) {
                  
                  Collection contents;
                  try {
                      contents = path.getChildren();
                  } catch (SourceException e) {
                      throw new SAXException("Error adding paths", e);
                  }
  
                  if (sort.equals("name")) {
                      Arrays.sort(contents.toArray(), new Comparator() {
                          public int compare(Object o1, Object o2) {
                              if (reverse) {
                                  return ((TraversableSource)o2).getName().compareTo(((TraversableSource)o1).getName());
                              }
                              return ((TraversableSource)o1).getName().compareTo(((TraversableSource)o2).getName());
                          }
                      });
                  } else if (sort.equals("size")) {
                      Arrays.sort(contents.toArray(), new Comparator() {
                          public int compare(Object o1, Object o2) {
                              if (reverse) {
                                  return new Long(((TraversableSource)o2).getContentLength()).compareTo(
                                      new Long(((TraversableSource)o1).getContentLength()));
                              }
                              return new Long(((TraversableSource)o1).getContentLength()).compareTo(
                                  new Long(((TraversableSource)o2).getContentLength()));
                          }
                      });
                  } else if (sort.equals("lastmodified")) {
                      Arrays.sort(contents.toArray(), new Comparator() {
                          public int compare(Object o1, Object o2) {
                              if (reverse) {
                                  return new Long(((TraversableSource)o2).getLastModified()).compareTo(
                                      new Long(((TraversableSource)o1).getLastModified()));
                              }
                              return new Long(((TraversableSource)o1).getLastModified()).compareTo(
                                  new Long(((TraversableSource)o2).getLastModified()));
                          }
                      });
                  } else if (sort.equals("collection")) {
                      Arrays.sort(contents.toArray(), new Comparator() {
                          public int compare(Object o1, Object o2) {
                              TraversableSource ts1 = (TraversableSource)o1;
                              TraversableSource ts2 = (TraversableSource)o2;
  
                              if (reverse) {
                                  if (ts2.isCollection() && !ts1.isCollection())
                                      return -1;
                                  if (!ts2.isCollection() && ts1.isCollection())
                                      return 1;
                                  return ts2.getName().compareTo(ts1.getName());
                              }
                              if (ts2.isCollection() && !ts1.isCollection())
                                  return 1;
                              if (!ts2.isCollection() && ts1.isCollection())
                                  return -1;
                              return ts1.getName().compareTo(ts2.getName());
                          }
                      });
                  }
  
                  for (int i = 0; i < contents.size(); i++) {
                      if (isIncluded((TraversableSource) contents.toArray()[i]) && !isExcluded((TraversableSource) contents.toArray()[i])) {
                          addPath((TraversableSource) contents.toArray()[i], depth - 1);
                      }
                  }
              }
              endNode(COL_NODE_NAME);
          } else {
              if (isIncluded(path) && !isExcluded(path)) {
                  startNode(RESOURCE_NODE_NAME, path);
                  endNode(RESOURCE_NODE_NAME);
              }
          }
      }
  
      /**
       * Begins a named node and calls setNodeAttributes to set its attributes.
       *
       * @param nodeName  the name of the new node
       * @param path      the file/directory to use when setting attributes
       * 
       * @throws SAXException  if an error occurs while creating the node
       */
      protected void startNode(String nodeName, TraversableSource path) throws SAXException {
          if (this.validity != null) {
              this.validity.addSource(path);
          }
          setNodeAttributes(path);
          super.contentHandler.startElement(URI, nodeName, PREFIX + ':' + nodeName, attributes);
      }
  
      /**
       * Sets the attributes for a given path. The default method sets attributes
       * for the name of thefile/directory and for the last modification time
       * of the path.
       *
       * @param path  the file/directory to use when setting attributes
       *
       * @throws SAXException  if an error occurs while setting the attributes
       */
      protected void setNodeAttributes(TraversableSource path) throws SAXException {
          long lastModified = path.getLastModified();
          attributes.clear();
          attributes.addAttribute("", RES_NAME_ATTR_NAME, RES_NAME_ATTR_NAME,
                                  "CDATA", path.getName());
          attributes.addAttribute("", LASTMOD_ATTR_NAME, LASTMOD_ATTR_NAME,
                                  "CDATA", Long.toString(path.getLastModified()));
          attributes.addAttribute("", DATE_ATTR_NAME, DATE_ATTR_NAME,
                                  "CDATA", dateFormatter.format(new Date(lastModified)));
          attributes.addAttribute("", SIZE_ATTR_NAME, SIZE_ATTR_NAME,
                                  "CDATA", Long.toString(path.getContentLength()));
          if (this.isRequestedCollection) {
              attributes.addAttribute("", "sort", "sort", "CDATA", this.sort);
              attributes.addAttribute("", "reverse", "reverse", "CDATA",
                                      String.valueOf(this.reverse));
              attributes.addAttribute("", "requested", "requested", "CDATA", "true");
              this.isRequestedCollection = false;
          }
      }
  
      /**
       * Ends the named node.
       *
       * @param nodeName  the name of the new node
       *
       * @throws SAXException  if an error occurs while closing the node
       */
      protected void endNode(String nodeName) throws SAXException {
          super.contentHandler.endElement(URI, nodeName, PREFIX + ':' + nodeName);
      }
  
      /**
       * Determines if a given File is the defined root.
       *
       * @param path  the File to check
       *
       * @return true if the File is the root or the root pattern is not set,
       *         false otherwise.
       */
      protected boolean isRoot(TraversableSource path) {
          return (this.rootRE == null) ? true : this.rootRE.match(path.getURI());
      }
  
      /**
       * Determines if a given File shall be visible.
       *
       * @param path  the File to check
       *
       * @return true if the File shall be visible or the include Pattern is <code>null</code>,
       *         false otherwise.
       */
      protected boolean isIncluded(TraversableSource path) {
          return (this.includeRE == null) ? true : this.includeRE.match(path.getURI());
      }
  
      /**
       * Determines if a given File shall be excluded from viewing.
       *
       * @param path  the File to check
       *
       * @return false if the given File shall not be excluded or the exclude Pattern is <code>null</code>,
       *         true otherwise.
       */
      protected boolean isExcluded(TraversableSource path) {
          return (this.excludeRE == null) ? false : this.excludeRE.match(path.getURI());
      }
  
      /**
       * Recycle resources
       */
      public void recycle() {
          super.recycle();
          this.attributes = null;
          this.dateFormatter = null;
          this.rootRE = null;
          this.includeRE = null;
          this.excludeRE = null;
          this.validity = null;
      }
  
      /** Specific validity class, that holds all files that have been generated */
      public static class CollectionValidity implements SourceValidity {
  
          private long expiry;
          private long delay;
          List sources = new ArrayList();
          List resourceDates = new ArrayList();
  
          public CollectionValidity(long delay) {
              expiry = System.currentTimeMillis() + delay;
              this.delay = delay;
          }
  
          public int isValid() {
              if (System.currentTimeMillis() <= expiry) {
                  return 1;
              }
  
              expiry = System.currentTimeMillis() + delay;
              int len = sources.size();
              for (int i = 0; i < len; i++) {
                  Source f = (Source)sources.get(i);
                  if (!f.exists()) {
                      return -1; // File was removed
                  }
  
                  long oldDate = ((Long)resourceDates.get(i)).longValue();
                  long newDate = f.getLastModified();
  
                  if (oldDate != newDate) {
                      return -1;
                  }
              }
  
              // all content is up to date: update the expiry date
              expiry = System.currentTimeMillis() + delay;
              return 1;
          }
  
          public int isValid(SourceValidity newValidity) {
              return isValid();
          }
  
          public void addSource(TraversableSource f) {
              sources.add(f);
              resourceDates.add(new Long(f.getLastModified()));
          }
      }
  }
  
  
  
  1.1                  cocoon-2.1/src/scratchpad/src/org/apache/cocoon/generation/XPathTraversableGenerator.java
  
  Index: XPathTraversableGenerator.java
  ===================================================================
  /*
  
   ============================================================================
                     The Apache Software License, Version 1.1
   ============================================================================
  
   Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.
  
   Redistribution and use in source and binary forms, with or without modifica-
   tion, are permitted provided that the following conditions are met:
  
   1. Redistributions of  source code must  retain the above copyright  notice,
      this list of conditions and the following disclaimer.
  
   2. Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
  
   3. The end-user documentation included with the redistribution, if any, must
      include  the following  acknowledgment:  "This product includes  software
      developed  by the  Apache Software Foundation  (http://www.apache.org/)."
      Alternately, this  acknowledgment may  appear in the software itself,  if
      and wherever such third-party acknowledgments normally appear.
  
   4. The names "Apache Cocoon" and  "Apache Software Foundation" must  not  be
      used to  endorse or promote  products derived from  this software without
      prior written permission. For written permission, please contact
      apache@apache.org.
  
   5. Products  derived from this software may not  be called "Apache", nor may
      "Apache" appear  in their name,  without prior written permission  of the
      Apache Software Foundation.
  
   THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
   FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
   APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
   INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
   DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
   OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
   ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
   (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
   This software  consists of voluntary contributions made  by many individuals
   on  behalf of the Apache Software  Foundation and was  originally created by
   Stefano Mazzocchi  <st...@apache.org>. For more  information on the Apache
   Software Foundation, please see <http://www.apache.org/>.
  
  */
  package org.apache.cocoon.generation;
  
  import java.io.IOException;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.Map;
  
  import org.apache.avalon.framework.component.ComponentManager;
  import org.apache.avalon.framework.parameters.Parameters;
  import org.apache.cocoon.ProcessingException;
  import org.apache.cocoon.environment.SourceResolver;
  import org.apache.cocoon.xml.dom.DOMStreamer;
  import org.apache.excalibur.source.SourceException;
  import org.apache.excalibur.source.TraversableSource;
  import org.apache.excalibur.xml.dom.DOMParser;
  import org.apache.excalibur.xml.xpath.PrefixResolver;
  import org.apache.excalibur.xml.xpath.XPathProcessor;
  import org.w3c.dom.Document;
  import org.w3c.dom.NodeList;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  import org.xml.sax.helpers.AttributesImpl;
  
  /**
   * Generates an XML directory listing performing XPath queries
   * on XML files. It can be used both as a plain TraversableGenerator
   * or, using an "xpointerinsh" syntax it will perform an XPath
   * query on every XML resource.
   *
   * Sample usage:
   *
   * Sitemap:
   * &lt;map:match pattern="documents/**"&gt;
   *   &lt;map:generate type="xpathdirectory"
   *     src="docs/{1}#/article/title|/article/abstract" /&gt;
   *   &lt;map:serialize type="xml" /&gt;
   * &lt;/map:match&gt;
   *
   * Request:
   *   http://www.some.host/documents/test
   * Result:
   * &lt;dir:directory
   *   name="test" lastModified="1010400942000"
   *   date="1/7/02 11:55 AM" requested="true"
   *   xmlns:dir="http://apache.org/cocoon/directory/2.0"&gt;
   *   &lt;dir:directory name="subdirectory" lastModified="1010400942000" date="1/7/02 11:55 AM" /&gt;
   *   &lt;dir:file name="test.xml" lastModified="1011011579000" date="1/14/02 1:32 PM"&gt;
   *     &lt;dir:xpath docid="test.xml" query="/article/title"&gt;
   *       &lt;title&gt;This is a test document&lt;/title&gt;
   *       &lt;abstract&gt;
   *         &lt;para&gt;Abstract of my test article&lt;/para&gt;
   *       &lt;/abstract&gt;
   *     &lt;/dir:xpath&gt;
   *   &lt;/dir:file&gt;
   *   &lt;dir:file name="test.gif" lastModified="1011011579000" date="1/14/02 1:32 PM"&gt;
   * &lt;/dir:directory&gt;
   * 
   * If you need to use namespaces, you can set them as sitemap parameters in
   * the form:
   * lt;map:parameter name="xmlns:<i>your prefix</i>" value="nsURI"/**"&gt; 
   *
   * @author <a href="mailto:gianugo@apache.org">Gianugo Rabellino</a>
   * @author <a href="mailto:d.madama@pro-netics.com">Daniele Madama</a>
   * @version CVS $Id: XPathTraversableGenerator.java,v 1.1 2003/07/10 08:12:49 gianugo Exp $
   */
  public class XPathTraversableGenerator extends TraversableGenerator {
  
      /** Element &lt;result&gt; */
      protected static final String RESULT = "xpath";
      protected static final String QRESULT = PREFIX + ":" + RESULT;
      protected static final String RESULT_DOCID_ATTR = "docid";
      protected static final String QUERY_ATTR = "query";
  
      protected static final String CDATA  = "CDATA";
      protected String XPathQuery = null;
      protected XPathProcessor processor = null;
      protected DOMParser parser;
      protected Document doc;
      private XPathPrefixResolver prefixResolver;
  	
      public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
          throws ProcessingException, SAXException, IOException {
          super.setup(resolver, objectModel, src, par);
          // See if an XPath was specified
          int pointer;
          if ((pointer = this.source.indexOf("#")) != -1) {
            int endpointer = this.source.indexOf('?');
            if (endpointer != -1) {
            	this.XPathQuery = source.substring(pointer + 1, endpointer); 
            } else {
  			this.XPathQuery = source.substring(pointer + 1);
            }
            this.source = src.substring(0, pointer);
            if (endpointer != -1)
            	this.source += src.substring(endpointer);
            if (this.getLogger().isDebugEnabled())
              this.getLogger().debug("Applying XPath: " + XPathQuery
                + " to collection " + source);
          }
          
          String[] params = par.getNames();
          this.prefixResolver = new XPathPrefixResolver();
          for (int i = 0; i < params.length; i++) {
              if (params[i].startsWith("xmlns:")) {
              	String paramValue = par.getParameter(params[i], "");
              	String paramName = params[i].substring(6);
              	if (getLogger().isDebugEnabled()) {
              		getLogger().debug("add param to prefixResolver: " + paramName);
              	}
              	this.prefixResolver.addPrefix(paramName, paramValue);
              }
          }
      }
  
      public void compose(ComponentManager manager) {
        try {
          super.compose(manager);
          processor = (XPathProcessor)manager.lookup(XPathProcessor.ROLE);
          parser = (DOMParser)manager.lookup(DOMParser.ROLE);
        } catch (Exception e) {
          this.getLogger().error("Could not obtain a required component", e);
        }
      }
  
      /**
       * Adds a single node to the generated document. If the path is a
       * directory, and depth is greater than zero, then recursive calls
       * are made to add nodes for the directory's children. Moreover,
       * if the file is an XML file (ends with .xml), the XPath query
       * is performed and results returned.
       *
       * @param   path
       *      the (Traversable) source to process
       * @param   depth
       *      how deep to scan the collection
       *
       * @throws  SAXException
       *      if an error occurs while constructing nodes
       */
      protected void addPath(TraversableSource path, int depth)
      throws SAXException {
  		if (path.isCollection()) {
  			startNode(COL_NODE_NAME, path);
  			if (depth > 0) {
                  
  				Collection contents;
  				try {
  					contents = path.getChildren();
  				} catch (SourceException e) {
  					throw new SAXException("Error adding paths", e);
  				}
  
  				if (sort.equals("name")) {
  					Arrays.sort(contents.toArray(), new Comparator() {
  						public int compare(Object o1, Object o2) {
  							if (reverse) {
  								return ((TraversableSource)o2).getName().compareTo(((TraversableSource)o1).getName());
  							}
  							return ((TraversableSource)o1).getName().compareTo(((TraversableSource)o2).getName());
  						}
  					});
  				} else if (sort.equals("size")) {
  					Arrays.sort(contents.toArray(), new Comparator() {
  						public int compare(Object o1, Object o2) {
  							if (reverse) {
  								return new Long(((TraversableSource)o2).getContentLength()).compareTo(
  									new Long(((TraversableSource)o1).getContentLength()));
  							}
  							return new Long(((TraversableSource)o1).getContentLength()).compareTo(
  								new Long(((TraversableSource)o2).getContentLength()));
  						}
  					});
  				} else if (sort.equals("lastmodified")) {
  					Arrays.sort(contents.toArray(), new Comparator() {
  						public int compare(Object o1, Object o2) {
  							if (reverse) {
  								return new Long(((TraversableSource)o2).getLastModified()).compareTo(
  									new Long(((TraversableSource)o1).getLastModified()));
  							}
  							return new Long(((TraversableSource)o1).getLastModified()).compareTo(
  								new Long(((TraversableSource)o2).getLastModified()));
  						}
  					});
  				} else if (sort.equals("directory")) {
  					Arrays.sort(contents.toArray(), new Comparator() {
  						public int compare(Object o1, Object o2) {
  							TraversableSource ts1 = (TraversableSource)o1;
  							TraversableSource ts2 = (TraversableSource)o2;
  
  							if (reverse) {
  								if (ts2.isCollection() && !ts1.isCollection())
  									return -1;
  								if (!ts2.isCollection() && ts1.isCollection())
  									return 1;
  								return ts2.getName().compareTo(ts1.getName());
  							}
  							if (ts2.isCollection() && !ts1.isCollection())
  								return 1;
  							if (!ts2.isCollection() && ts1.isCollection())
  								return -1;
  							return ts1.getName().compareTo(ts2.getName());
  						}
  					});
  				}
  
  				for (int i = 0; i < contents.size(); i++) {
  					if (isIncluded((TraversableSource) contents.toArray()[i]) && !isExcluded((TraversableSource) contents.toArray()[i])) {
  						addPath((TraversableSource) contents.toArray()[i], depth - 1);
  					}
  				}
  			}
  			endNode(COL_NODE_NAME);
  		} else {
  			if (isIncluded(path) && !isExcluded(path)) {
  				startNode(RESOURCE_NODE_NAME, path);
  				if (path.getName().endsWith(".xml") && XPathQuery != null)
  				  performXPathQuery(path);
  				endNode(RESOURCE_NODE_NAME);
  			}
  		}
      }
  
      protected void performXPathQuery(TraversableSource in)
        throws SAXException {
        doc = null;
        try {
          doc = parser.parseDocument(new InputSource(in.getInputStream()));
        } catch (SAXException se) {
           this.getLogger().error("Warning:" + in.getName()
            + " is not a valid XML document. Ignoring");
        } catch (Exception e) {
           this.getLogger().error("Unable to resolve and parse document" + e);
         }
         if (doc != null) {
           NodeList nl = processor.selectNodeList(doc.getDocumentElement(), XPathQuery, this.prefixResolver);
           final String id = in.getName();
           AttributesImpl attributes = new AttributesImpl();
           attributes.addAttribute("", RESULT_DOCID_ATTR, RESULT_DOCID_ATTR,
             CDATA, id);
           attributes.addAttribute("", QUERY_ATTR, QUERY_ATTR, CDATA,
             XPathQuery);
           super.contentHandler.startElement(URI, RESULT, QRESULT, attributes);
           DOMStreamer ds = new DOMStreamer(super.xmlConsumer);
           for (int i = 0; i < nl.getLength(); i++)
             ds.stream(nl.item(i));
           super.contentHandler.endElement(URI, RESULT, QRESULT);
        }
      }
  
      /**
       * Recycle resources
       *
       */
     public void recycle() {
        super.recycle();
        this.XPathQuery = null;
        this.attributes = null;
        this.doc = null;
      }
  
      /**
       * A brain-dead PrefixResolver implementation
       * 
       */
      
      class XPathPrefixResolver implements PrefixResolver {
      	
      	private Map params;
  
          public XPathPrefixResolver() {
          	this.params = new HashMap();
          }
  
          /**
           * Get a namespace URI given a prefix.
           * 
           * @see org.apache.excalibur.xml.xpath.PrefixResolver#prefixToNamespace(java.lang.String)
           */
          public String prefixToNamespace(String prefix) {
          	if (getLogger().isDebugEnabled()) {
          		getLogger().debug("prefix: " + prefix);
          	}
          	if (this.params.containsKey(prefix)) {
          		if(getLogger().isDebugEnabled()) {
          			getLogger().debug("prefix; " + prefix + " - namespace: " + this.params.get(prefix));
          		}
          		return (String) this.params.get(prefix);
          	}
              return null;
          }
      	
      	public void addPrefix(String prefix, String uri) {    		
      		this.params.put(prefix, uri);
      	}
  
      }
      
  }