blob: 7ece8afdef420ca2ffc8a6cbab7eb59430008a02 [file] [log] [blame]
/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.wyona.cms.cocoon.generation;
import org.apache.avalon.excalibur.pool.Recyclable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.ResourceNotFoundException;
import org.apache.cocoon.environment.Source;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.generation.ComposerGenerator;
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Stack;
/**
* Generates an XML directory listing.
* <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> 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>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.
* </dl>
*
* @author <a href="mailto:fumagalli@exoffice.com">Pierpaolo Fumagalli</a>
* (Apache Software Foundation, Exoffice Technologies)
* @author <a href="mailto:conny@smb-tec.com">Conny Krappatsch</a>
* (SMB GmbH) for Virbus AG
* @version CVS $Revision: 1.1 $ $Date: 2002/01/11 14:52:58 $ */
public class DirectoryGenerator extends ComposerGenerator implements Recyclable {
private static final String FILE = "file:";
/** The URI of the namespace of this generator. */
protected static final String URI =
"http://apache.org/cocoon/directory/2.0";
/** The namespace prefix for this namespace. */
protected static final String PREFIX = "dir";
/* Node and attribute names */
protected static final String DIR_NODE_NAME = "directory";
protected static final String FILE_NODE_NAME = "file";
protected static final String FILENAME_ATTR_NAME = "name";
protected static final String LASTMOD_ATTR_NAME = "lastModified";
protected static final String DATE_ATTR_NAME = "date";
/*
* Variables set per-request
*
* FIXME: SimpleDateFormat is not supported by all locales!
*/
protected int depth;
protected AttributesImpl attributes = new AttributesImpl();
protected SimpleDateFormat dateFormatter;
protected RE rootRE;
protected RE includeRE;
protected RE excludeRE;
protected boolean isRequestedDirectory;
/**
* 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 URI for this request (?)
* @param par
* configuration parameters
*/
public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
throws ProcessingException, SAXException, IOException {
super.setup(resolver, objectModel, src, par);
String dateFormatString = par.getParameter("dateFormat", null);
if (dateFormatString != null) {
this.dateFormatter = new SimpleDateFormat(dateFormatString);
} else {
this.dateFormatter = new SimpleDateFormat();
}
this.depth = par.getParameterAsInteger("depth", 1);
getLogger().debug("depth: " + this.depth);
String rePattern = par.getParameter("root", null);
try {
getLogger().debug("root pattern: " + rePattern);
this.rootRE = (rePattern == null)?null:new RE(rePattern);
rePattern = par.getParameter("include", null);
getLogger().debug("include pattern: " + rePattern);
this.includeRE = (rePattern == null)?null:new RE(rePattern);
rePattern = par.getParameter("exclude", null);
getLogger().debug("exclude pattern: " + rePattern);
this.excludeRE = (rePattern == null)?null:new RE(rePattern);
} catch (RESyntaxException rese) {
getLogger().error("Syntax error in regexp pattern '" + rePattern + "'", rese);
throw new ProcessingException("Syntax error in regexp pattern '"
+ rePattern + "'", rese);
}
this.isRequestedDirectory = false;
/* Create a reusable attributes for creating nodes */
this.attributes = new AttributesImpl();
}
/**
* 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 {
getLogger().error("HALLO RAFI");
String directory = super.source;
Source inputSource = null;
try {
inputSource = this.resolver.resolve(directory);
String systemId = inputSource.getSystemId();
if (!systemId.startsWith(FILE)) {
throw new ResourceNotFoundException(systemId + " does not denote a directory");
}
// This relies on systemId being of the form "file://..."
File directoryFile = new File(new URL(systemId).getFile());
if (!directoryFile.isDirectory()) {
throw new ResourceNotFoundException(directory + " is not a directory.");
}
this.contentHandler.startDocument();
this.contentHandler.startPrefixMapping(PREFIX,URI);
Stack ancestors = getAncestors(directoryFile);
addPathWithAncestors(directoryFile, ancestors);
this.contentHandler.endPrefixMapping(PREFIX);
this.contentHandler.endDocument();
} catch (IOException ioe) {
getLogger().warn("Could not read directory " + directory, ioe);
getLogger().error("Could not read directory " + directory, ioe);
throw new ResourceNotFoundException("Could not read directory "
+ directory, ioe);
} finally {
if (inputSource != null) inputSource.recycle();
}
}
/**
* 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(File path) {
File parent = path;
Stack ancestors = new Stack();
while ((parent != null) && !isRoot(parent)) {
parent = parent.getParentFile();
if (parent != null) {
ancestors.push(parent);
}
}
return ancestors;
}
protected void addPathWithAncestors(File path, Stack ancestors)
throws SAXException {
if (ancestors.empty()) {
this.isRequestedDirectory = true;
addPath(path, depth);
} else {
startNode(DIR_NODE_NAME, (File)ancestors.pop());
addPathWithAncestors(path, ancestors);
endNode(DIR_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(File path, int depth)
throws SAXException {
if (path.isDirectory()) {
startNode(DIR_NODE_NAME, path);
if (depth>0) {
File contents[] = path.listFiles();
for (int i=0; i<contents.length; i++) {
if (isIncluded(contents[i]) && !isExcluded(contents[i])) {
addPath(contents[i], depth-1);
}
}
}
endNode(DIR_NODE_NAME);
} else {
if (isIncluded(path) && !isExcluded(path)) {
startNode(FILE_NODE_NAME, path);
endNode(FILE_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, File path)
throws SAXException {
setNodeAttributes(path);
super.contentHandler.startElement(URI, nodeName, URI+':'+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(File path) throws SAXException {
long lastModified = path.lastModified();
attributes.clear();
attributes.addAttribute("", FILENAME_ATTR_NAME,
FILENAME_ATTR_NAME, "CDATA",
path.getName());
attributes.addAttribute("", LASTMOD_ATTR_NAME,
LASTMOD_ATTR_NAME, "CDATA",
Long.toString(path.lastModified()));
attributes.addAttribute("", DATE_ATTR_NAME,
DATE_ATTR_NAME, "CDATA",
dateFormatter.format(new Date(lastModified)));
if (this.isRequestedDirectory) {
attributes.addAttribute("", "requested", "requested", "CDATA",
"true");
this.isRequestedDirectory = false;
}
}
/**
* Ends the named node.
*
* @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 closing the node
*/
protected void endNode(String nodeName)
throws SAXException {
super.contentHandler.endElement(URI, nodeName, URI+':'+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(File path) {
return (this.rootRE == null)
? true
: this.rootRE.match(path.getName());
}
/**
* 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(File path) {
return (this.includeRE == null)
? true
: this.includeRE.match(path.getName());
}
/**
* 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(File path) {
return (this.excludeRE == null)
? false
: this.excludeRE.match(path.getName());
}
}