blob: 06ca53660ce17ff208c9e87380e142d82e5c7829 [file] [log] [blame]
/*
* Copyright 1999-2005 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.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.components.source.impl.MultiSourceValidity;
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.Locale;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Arrays;
import java.util.Comparator;
import java.util.TimeZone;
/**
* Generates an XML source hierarchy listing from a Traversable Source.
* <p>
* The root node of the generated document will normally be a
* <code>collection</code> node and a collection node can contain zero or more
* <code>resource</code> or collection nodes. A resource node has no children.
* Each node will contain the following attributes:
* <blockquote>
* <dl>
* <dt> name
* <dd> the name of the source
* <dt> lastModified
* <dd> the time the source was last modified, measured as the number of
* milliseconds since the epoch (as in java.io.File.lastModified)
* <dt> size
* <dd> the source size, in bytes (as in java.io.File.length)
* <dt> date (optional)
* <dd> the time the source was last modified in human-readable form
* </dl>
* </blockquote>
* <p>
* <b>Configuration options:</b>
* <dl>
* <dt> <i>depth</i> (optional)
* <dd> Sets how deep TraversableGenerator should delve into the
* source hierarchy. If set to 1 (the default), only the starting
* collection'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 collection 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>timeZone</i> (optional)
* <dd> Sets the time zone offset ID for the date attribute, as
* described in java.util.TimeZone. If unset, the default
* system time zone will be used.
* <dt> <i>refreshDelay</i> (optional)
* <dd> Sets the delay (in seconds) between checks on the source hierarchy
* for changed content. Defaults to 1 second.
* </dl>
* </p>
*
* @version $Id$
*/
public class TraversableGenerator extends ServiceableGenerator
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 = "resource";
protected static final String RES_NAME_ATTR_NAME = "name";
protected static final String URI_ATTR_NAME = "uri";
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 MultiSourceValidity validity;
/**
* Convenience object, so we don't need to create an AttributesImpl for every element.
*/
protected AttributesImpl attributes;
/**
* The cache key needs to be generated for the configuration of this
* generator, so storing the parameters for generateKey().
* Using the member variables after setup() would not work I guess. I don't
* know a way from the regular expressions back to the pattern or at least
* a useful string.
*/
protected List cacheKeyParList;
/**
* The depth parameter determines how deep the TraversableGenerator 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 source hierarchy. */
protected long refreshDelay;
/**
* The sort parameter determines by which attribute the content of one
* collection should be sorted. Possible values are "name", "size", "time"
* and "collection", where "collection" is the same as "name", except that
* collection 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 source specified by the
* <code>src</code> attribute on the generator's configuration.
*/
protected boolean isRequestedSource;
/**
* 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 Traversable Source to be XMLized specified as
* <code>src</code> 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.cacheKeyParList = new ArrayList();
this.cacheKeyParList.add(src);
this.depth = par.getParameterAsInteger("depth", 1);
this.cacheKeyParList.add(String.valueOf(this.depth));
String dateFormatString = par.getParameter("dateFormat", null);
this.cacheKeyParList.add(dateFormatString);
if (dateFormatString != null) {
String locale = par.getParameter("locale", null);
if (locale != null) {
this.dateFormatter = new SimpleDateFormat(dateFormatString, new Locale(locale, ""));
} else {
this.dateFormatter = new SimpleDateFormat(dateFormatString);
}
} else {
this.dateFormatter = new SimpleDateFormat();
}
String timeZone = par.getParameter("timeZone", null);
if (timeZone != null) {
this.dateFormatter.setTimeZone(TimeZone.getTimeZone(timeZone));
}
this.sort = par.getParameter("sort", "name");
this.cacheKeyParList.add(this.sort);
this.reverse = par.getParameterAsBoolean("reverse", false);
this.cacheKeyParList.add(String.valueOf(this.reverse));
this.refreshDelay = par.getParameterAsLong("refreshDelay", 1L) * 1000L;
this.cacheKeyParList.add(String.valueOf(this.refreshDelay));
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("depth: " + this.depth);
this.getLogger().debug("dateFormat: " + this.dateFormatter.toPattern());
this.getLogger().debug("timeZone: " + timeZone);
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);
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("root pattern: " + rePattern);
}
this.cacheKeyParList.add(rePattern);
this.rootRE = (rePattern == null) ? null : new RE(rePattern);
rePattern = par.getParameter("include", null);
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("include pattern: " + rePattern);
}
this.cacheKeyParList.add(rePattern);
this.includeRE = (rePattern == null) ? null : new RE(rePattern);
rePattern = par.getParameter("exclude", null);
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("exclude pattern: " + rePattern);
}
this.cacheKeyParList.add(rePattern);
this.excludeRE = (rePattern == null) ? null : new RE(rePattern);
} catch (RESyntaxException rese) {
throw new ProcessingException("Syntax error in regexp pattern '"
+ rePattern + "'", rese);
}
this.isRequestedSource = false;
this.attributes = new AttributesImpl();
}
/* (non-Javadoc)
* @see org.apache.cocoon.caching.CacheableProcessingComponent#getKey()
*/
public Serializable getKey() {
StringBuffer buffer = new StringBuffer();
int len = this.cacheKeyParList.size();
for (int i = 0; i < len; i++) {
buffer.append(this.cacheKeyParList.get(i));
buffer.append(':');
}
return buffer.toString();
}
/**
* Gets the source validity, using a deferred validity object. The validity
* is initially empty since the resources 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 resources that is traversed.
*
* @see org.apache.cocoon.components.source.impl.MultiSourceValidity
*/
public SourceValidity getValidity() {
if (this.validity == null) {
this.validity = new MultiSourceValidity(this.resolver, this.refreshDelay);
}
return this.validity;
}
/**
* Generate XML data.
*
* @throws SAXException if an error occurs while outputting the document
* @throws ProcessingException if something went wrong while traversing
* the source hierarchy
*/
public void generate() throws SAXException, ProcessingException {
Source src = null;
Stack ancestors = null;
try {
src = this.resolver.resolveURI(this.source);
if (!(src instanceof TraversableSource)) {
throw new SourceException(this.source + " is not a traversable source");
}
final TraversableSource inputSource =
(TraversableSource) this.resolver.resolveURI(this.source);
if (!inputSource.exists()) {
throw new ResourceNotFoundException(this.source + " does not exist.");
}
this.contentHandler.startDocument();
this.contentHandler.startPrefixMapping(PREFIX, URI);
ancestors = getAncestors(inputSource);
addAncestorPath(inputSource, ancestors);
this.contentHandler.endPrefixMapping(PREFIX);
this.contentHandler.endDocument();
if (this.validity != null) {
this.validity.close();
}
} catch (SourceException se) {
throw SourceUtil.handle(se);
} catch (IOException ioe) {
throw new ResourceNotFoundException("Could not read collection "
+ this.source, ioe);
} finally {
if (src != null) {
this.resolver.release(src);
}
if (ancestors != null) {
Enumeration enumeration = ancestors.elements();
while (enumeration.hasMoreElements()) {
resolver.release((Source) enumeration.nextElement());
}
}
}
}
/**
* Creates a stack containing the ancestors of a traversable source up to
* specific parent matching the root pattern.
*
* @param source the traversable source whose ancestors shall be retrieved
* @return a Stack containing the ancestors.
*/
protected Stack getAncestors(TraversableSource source) throws IOException {
TraversableSource parent = source;
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 source matched by the root pattern
* down to the requested source.
*
* @param source the requested source.
* @param ancestors the stack of the ancestors.
* @throws SAXException
* @throws ProcessingException
*/
protected void addAncestorPath(TraversableSource source, Stack ancestors)
throws SAXException, ProcessingException {
if (ancestors.empty()) {
this.isRequestedSource = true;
addPath(source, depth);
} else {
startNode(COL_NODE_NAME, (TraversableSource) ancestors.pop());
addAncestorPath(source, ancestors);
endNode(COL_NODE_NAME);
}
}
/**
* Adds a single node to the generated document. If the path is a
* collection and depth is greater than zero, then recursive calls
* are made to add nodes for the collection's children.
*
* @param source the resource/collection to process
* @param depth how deep to scan the collection hierarchy
*
* @throws SAXException if an error occurs while constructing nodes
* @throws ProcessingException if a problem occurs with the source
*/
protected void addPath(TraversableSource source, int depth)
throws SAXException, ProcessingException {
if (source.isCollection()) {
startNode(COL_NODE_NAME, source);
addContent(source);
if (depth > 0) {
Collection contents = null;
try {
contents = source.getChildren();
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);
}
}
} catch (SourceException e) {
throw new ProcessingException("Error adding paths", e);
} finally {
if (contents != null) {
Iterator iter = contents.iterator();
while (iter.hasNext()) {
resolver.release((Source) iter.next());
}
}
}
}
endNode(COL_NODE_NAME);
} else {
if (isIncluded(source) && !isExcluded(source)) {
startNode(RESOURCE_NODE_NAME, source);
addContent(source);
endNode(RESOURCE_NODE_NAME);
}
}
}
/**
* Allow subclasses a chance to generate additional elements within collection and resource
* elements.
*
* @param source the source to generate additional data for.
*/
protected void addContent(TraversableSource source) throws SAXException, ProcessingException {
}
/**
* Begins a named node and calls setNodeAttributes to set its attributes.
*
* @param nodeName the name of the new node
* @param source the source a node with its attributes is added for
*
* @throws SAXException if an error occurs while creating the node
*/
protected void startNode(String nodeName, TraversableSource source)
throws SAXException, ProcessingException {
if (this.validity != null) {
this.validity.addSource(source);
}
setNodeAttributes(source);
super.contentHandler.startElement(URI, nodeName, PREFIX + ':' + nodeName, attributes);
}
/**
* Sets the attributes for a given source. For example attributes for the
* name, the size and the last modification date of the source are added.
*
* @param source the source attributes are added for
*/
protected void setNodeAttributes(TraversableSource source)
throws SAXException, ProcessingException {
long lastModified = source.getLastModified();
attributes.clear();
attributes.addAttribute("", RES_NAME_ATTR_NAME,RES_NAME_ATTR_NAME,
"CDATA", source.getName());
attributes.addAttribute("", URI_ATTR_NAME,URI_ATTR_NAME,
"CDATA", source.getURI());
attributes.addAttribute("", LASTMOD_ATTR_NAME, LASTMOD_ATTR_NAME,
"CDATA", Long.toString(source.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(source.getContentLength()));
if (this.isRequestedSource) {
attributes.addAttribute("", "sort", "sort", "CDATA", this.sort);
attributes.addAttribute("", "reverse", "reverse", "CDATA",
String.valueOf(this.reverse));
attributes.addAttribute("", "requested", "requested", "CDATA", "true");
this.isRequestedSource = 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 source is the defined root.
*
* @param source the source to check
*
* @return true if the source is the root or the root pattern is not set,
* false otherwise.
*/
protected boolean isRoot(TraversableSource source) {
return this.rootRE == null ? true : this.rootRE.match(source.getName());
}
/**
* Determines if a given source shall be visible.
*
* @param source the source to check
*
* @return true if the source shall be visible or the include Pattern is not set,
* false otherwise.
*/
protected boolean isIncluded(TraversableSource source) {
return this.includeRE == null ? true : this.includeRE.match(source.getName());
}
/**
* Determines if a given source shall be excluded from viewing.
*
* @param source the source to check
*
* @return false if the given source shall not be excluded or the exclude Pattern is not set,
* true otherwise.
*/
protected boolean isExcluded(TraversableSource source) {
return this.excludeRE == null ? false : this.excludeRE.match(source.getName());
}
/**
* Recycle resources
*/
public void recycle() {
this.cacheKeyParList = null;
this.attributes = null;
this.dateFormatter = null;
this.rootRE = null;
this.includeRE = null;
this.excludeRE = null;
this.validity = null;
super.recycle();
}
}