001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.generation;
018:
019: import org.apache.avalon.framework.parameters.Parameters;
020: import org.apache.cocoon.ProcessingException;
021: import org.apache.cocoon.ResourceNotFoundException;
022: import org.apache.cocoon.caching.CacheableProcessingComponent;
023: import org.apache.cocoon.components.source.SourceUtil;
024: import org.apache.cocoon.environment.SourceResolver;
025: import org.apache.excalibur.source.Source;
026: import org.apache.excalibur.source.SourceException;
027: import org.apache.excalibur.source.SourceValidity;
028: import org.apache.regexp.RE;
029: import org.apache.regexp.RESyntaxException;
030: import org.xml.sax.SAXException;
031: import org.xml.sax.helpers.AttributesImpl;
032:
033: import java.io.File;
034: import java.io.IOException;
035: import java.io.Serializable;
036: import java.net.URL;
037: import java.text.SimpleDateFormat;
038: import java.util.ArrayList;
039: import java.util.Date;
040: import java.util.List;
041: import java.util.Map;
042: import java.util.Stack;
043: import java.util.Arrays;
044: import java.util.Comparator;
045:
046: /**
047: * @cocoon.sitemap.component.documentation
048: * Generates an XML directory listing.
049: * A more general approach is implemented by the TraversableGenerator (src/blocks/repository/java/org/apache/cocoon/generation/TraversableGenerator.java)
050: *
051: * @cocoon.sitemap.component.name directory
052: * @cocoon.sitemap.component.label content
053: * @cocoon.sitemap.component.logger sitemap.generator.directory
054: * @cocoon.sitemap.component.documentation.caching
055: * Uses the last modification date of the directory and the contained files
056: *
057: * @cocoon.sitemap.component.pooling.max 16
058: *
059: * @version $Id: DirectoryGenerator.java 433543 2006-08-22 06:22:54Z crossley $
060: */
061: public class DirectoryGenerator extends ServiceableGenerator implements
062: CacheableProcessingComponent {
063:
064: /** Constant for the file protocol. */
065: private static final String FILE = "file:";
066:
067: /** The URI of the namespace of this generator. */
068: protected static final String URI = "http://apache.org/cocoon/directory/2.0";
069:
070: /** The namespace prefix for this namespace. */
071: protected static final String PREFIX = "dir";
072:
073: /* Node and attribute names */
074: protected static final String DIR_NODE_NAME = "directory";
075: protected static final String FILE_NODE_NAME = "file";
076:
077: protected static final String FILENAME_ATTR_NAME = "name";
078: protected static final String LASTMOD_ATTR_NAME = "lastModified";
079: protected static final String DATE_ATTR_NAME = "date";
080: protected static final String SIZE_ATTR_NAME = "size";
081:
082: /** The validity that is being built */
083: protected DirValidity validity;
084: /** Convenience object, so we don't need to create an AttributesImpl for every element. */
085: protected AttributesImpl attributes;
086:
087: /**
088: * The cache key needs to be generated for the configuration of this
089: * generator, so storing the parameters for generateKey().
090: * Using the member variables after setup() would not work I guess. I don't
091: * know a way from the regular expressions back to the pattern or at least
092: * a useful string.
093: */
094: protected List cacheKeyParList;
095:
096: /** The depth parameter determines how deep the DirectoryGenerator should delve. */
097: protected int depth;
098: /**
099: * The dateFormatter determines into which date format the lastModified
100: * time should be converted.
101: * FIXME: SimpleDateFormat is not supported by all locales!
102: */
103: protected SimpleDateFormat dateFormatter;
104: /** The delay between checks on updates to the filesystem. */
105: protected long refreshDelay;
106: /**
107: * The sort parameter determines by which attribute the content of one
108: * directory should be sorted. Possible values are "name", "size", "lastmodified"
109: * and "directory", where "directory" is the same as "name", except that
110: * directory entries are listed first.
111: */
112: protected String sort;
113: /** The reverse parameter reverses the sort order. <code>false</code> is default. */
114: protected boolean reverse;
115: /** The regular expression for the root pattern. */
116: protected RE rootRE;
117: /** The regular expression for the include pattern. */
118: protected RE includeRE;
119: /** The regular expression for the exclude pattern. */
120: protected RE excludeRE;
121: /**
122: * This is only set to true for the requested directory specified by the
123: * <code>src</code> attribute on the generator's configuration.
124: */
125: protected boolean isRequestedDirectory;
126:
127: /** The source object for the directory. */
128: protected Source directorySource;
129:
130: /**
131: * Set the request parameters. Must be called before the generate method.
132: *
133: * @param resolver the SourceResolver object
134: * @param objectModel a <code>Map</code> containing model object
135: * @param src the directory to be XMLized specified as src attribute on <map:generate/>
136: * @param par configuration parameters
137: */
138: public void setup(SourceResolver resolver, Map objectModel,
139: String src, Parameters par) throws ProcessingException,
140: SAXException, IOException {
141: if (src == null) {
142: throw new ProcessingException(
143: "No src attribute pointing to a directory to be XMLized specified.");
144: }
145: super .setup(resolver, objectModel, src, par);
146:
147: try {
148: this .directorySource = this .resolver.resolveURI(src);
149: } catch (SourceException se) {
150: throw SourceUtil.handle(se);
151: }
152:
153: this .cacheKeyParList = new ArrayList();
154: this .cacheKeyParList.add(this .directorySource.getURI());
155:
156: this .depth = par.getParameterAsInteger("depth", 1);
157: this .cacheKeyParList.add(String.valueOf(this .depth));
158:
159: String dateFormatString = par.getParameter("dateFormat", null);
160: this .cacheKeyParList.add(dateFormatString);
161: if (dateFormatString != null) {
162: this .dateFormatter = new SimpleDateFormat(dateFormatString);
163: } else {
164: this .dateFormatter = new SimpleDateFormat();
165: }
166:
167: this .sort = par.getParameter("sort", "name");
168: this .cacheKeyParList.add(this .sort);
169:
170: this .reverse = par.getParameterAsBoolean("reverse", false);
171: this .cacheKeyParList.add(String.valueOf(this .reverse));
172:
173: this .refreshDelay = par.getParameterAsLong("refreshDelay", 1L) * 1000L;
174: this .cacheKeyParList.add(String.valueOf(this .refreshDelay));
175:
176: if (this .getLogger().isDebugEnabled()) {
177: this .getLogger().debug("depth: " + this .depth);
178: this .getLogger().debug(
179: "dateFormat: " + this .dateFormatter.toPattern());
180: this .getLogger().debug("sort: " + this .sort);
181: this .getLogger().debug("reverse: " + this .reverse);
182: this .getLogger()
183: .debug("refreshDelay: " + this .refreshDelay);
184: }
185:
186: String rePattern = null;
187: try {
188: rePattern = par.getParameter("root", null);
189: this .cacheKeyParList.add(rePattern);
190: this .rootRE = (rePattern == null) ? null
191: : new RE(rePattern);
192: if (this .getLogger().isDebugEnabled()) {
193: this .getLogger().debug("root pattern: " + rePattern);
194: }
195:
196: rePattern = par.getParameter("include", null);
197: this .cacheKeyParList.add(rePattern);
198: this .includeRE = (rePattern == null) ? null : new RE(
199: rePattern);
200: if (this .getLogger().isDebugEnabled()) {
201: this .getLogger().debug("include pattern: " + rePattern);
202: }
203:
204: rePattern = par.getParameter("exclude", null);
205: this .cacheKeyParList.add(rePattern);
206: this .excludeRE = (rePattern == null) ? null : new RE(
207: rePattern);
208: if (this .getLogger().isDebugEnabled()) {
209: this .getLogger().debug("exclude pattern: " + rePattern);
210: }
211: } catch (RESyntaxException rese) {
212: throw new ProcessingException(
213: "Syntax error in regexp pattern '" + rePattern
214: + "'", rese);
215: }
216:
217: this .isRequestedDirectory = false;
218: this .attributes = new AttributesImpl();
219: }
220:
221: /* (non-Javadoc)
222: * @see org.apache.cocoon.caching.CacheableProcessingComponent#getKey()
223: */
224: public Serializable getKey() {
225: StringBuffer buffer = new StringBuffer();
226: int len = this .cacheKeyParList.size();
227: for (int i = 0; i < len; i++) {
228: buffer.append((String) this .cacheKeyParList.get(i) + ":");
229: }
230: return buffer.toString();
231: }
232:
233: /**
234: * Gets the source validity, using a deferred validity object. The validity
235: * is initially empty since the files that define it are not known before
236: * generation has occured. So the returned object is kept by the generator
237: * and filled with each of the files that are traversed.
238: *
239: * @see DirectoryGenerator.DirValidity
240: */
241: public SourceValidity getValidity() {
242: if (this .validity == null) {
243: this .validity = new DirValidity(this .refreshDelay);
244: }
245: return this .validity;
246: }
247:
248: /**
249: * Generate XML data.
250: *
251: * @throws SAXException if an error occurs while outputting the document
252: * @throws ProcessingException if the requsted URI isn't a directory on the local filesystem
253: */
254: public void generate() throws SAXException, ProcessingException {
255: try {
256: String systemId = this .directorySource.getURI();
257: if (!systemId.startsWith(FILE)) {
258: throw new ResourceNotFoundException(systemId
259: + " does not denote a directory");
260: }
261: // This relies on systemId being of the form "file://..."
262: File directoryFile = new File(new URL(systemId).getFile());
263: if (!directoryFile.isDirectory()) {
264: throw new ResourceNotFoundException(super .source
265: + " is not a directory.");
266: }
267:
268: this .contentHandler.startDocument();
269: this .contentHandler.startPrefixMapping(PREFIX, URI);
270:
271: Stack ancestors = getAncestors(directoryFile);
272: addAncestorPath(directoryFile, ancestors);
273:
274: this .contentHandler.endPrefixMapping(PREFIX);
275: this .contentHandler.endDocument();
276: } catch (IOException ioe) {
277: throw new ResourceNotFoundException(
278: "Could not read directory " + super .source, ioe);
279: }
280: }
281:
282: /**
283: * Creates a stack containing the ancestors of File up to specified directory.
284: *
285: * @param path the File whose ancestors shall be retrieved
286: * @return a Stack containing the ancestors.
287: */
288: protected Stack getAncestors(File path) {
289: File parent = path;
290: Stack ancestors = new Stack();
291:
292: while ((parent != null) && !isRoot(parent)) {
293: parent = parent.getParentFile();
294: if (parent != null) {
295: ancestors.push(parent);
296: } else {
297: // no ancestor matched the root pattern
298: ancestors.clear();
299: }
300: }
301:
302: return ancestors;
303: }
304:
305: /**
306: * Adds recursively the path from the directory matched by the root pattern
307: * down to the requested directory.
308: *
309: * @param path the requested directory.
310: * @param ancestors the stack of the ancestors.
311: * @throws SAXException
312: */
313: protected void addAncestorPath(File path, Stack ancestors)
314: throws SAXException {
315: if (ancestors.empty()) {
316: this .isRequestedDirectory = true;
317: addPath(path, depth);
318: } else {
319: startNode(DIR_NODE_NAME, (File) ancestors.pop());
320: addAncestorPath(path, ancestors);
321: endNode(DIR_NODE_NAME);
322: }
323: }
324:
325: /**
326: * Adds a single node to the generated document. If the path is a
327: * directory, and depth is greater than zero, then recursive calls
328: * are made to add nodes for the directory's children.
329: *
330: * @param path the file/directory to process
331: * @param depth how deep to scan the directory
332: * @throws SAXException if an error occurs while constructing nodes
333: */
334: protected void addPath(File path, int depth) throws SAXException {
335: if (path.isDirectory()) {
336: startNode(DIR_NODE_NAME, path);
337: if (depth > 0) {
338: File contents[] = path.listFiles();
339:
340: if (sort.equals("name")) {
341: Arrays.sort(contents, new Comparator() {
342: public int compare(Object o1, Object o2) {
343: if (reverse) {
344: return ((File) o2).getName().compareTo(
345: ((File) o1).getName());
346: }
347: return ((File) o1).getName().compareTo(
348: ((File) o2).getName());
349: }
350: });
351: } else if (sort.equals("size")) {
352: Arrays.sort(contents, new Comparator() {
353: public int compare(Object o1, Object o2) {
354: if (reverse) {
355: return new Long(((File) o2).length())
356: .compareTo(new Long(((File) o1)
357: .length()));
358: }
359: return new Long(((File) o1).length())
360: .compareTo(new Long(((File) o2)
361: .length()));
362: }
363: });
364: } else if (sort.equals("lastmodified")) {
365: Arrays.sort(contents, new Comparator() {
366: public int compare(Object o1, Object o2) {
367: if (reverse) {
368: return new Long(((File) o2)
369: .lastModified())
370: .compareTo(new Long(((File) o1)
371: .lastModified()));
372: }
373: return new Long(((File) o1).lastModified())
374: .compareTo(new Long(((File) o2)
375: .lastModified()));
376: }
377: });
378: } else if (sort.equals("directory")) {
379: Arrays.sort(contents, new Comparator() {
380: public int compare(Object o1, Object o2) {
381: File f1 = (File) o1;
382: File f2 = (File) o2;
383:
384: if (reverse) {
385: if (f2.isDirectory() && f1.isFile())
386: return -1;
387: if (f2.isFile() && f1.isDirectory())
388: return 1;
389: return f2.getName().compareTo(
390: f1.getName());
391: }
392: if (f2.isDirectory() && f1.isFile())
393: return 1;
394: if (f2.isFile() && f1.isDirectory())
395: return -1;
396: return f1.getName().compareTo(f2.getName());
397: }
398: });
399: }
400:
401: for (int i = 0; i < contents.length; i++) {
402: if (isIncluded(contents[i])
403: && !isExcluded(contents[i])) {
404: addPath(contents[i], depth - 1);
405: }
406: }
407: }
408: endNode(DIR_NODE_NAME);
409: } else {
410: if (isIncluded(path) && !isExcluded(path)) {
411: startNode(FILE_NODE_NAME, path);
412: endNode(FILE_NODE_NAME);
413: }
414: }
415: }
416:
417: /**
418: * Begins a named node and calls setNodeAttributes to set its attributes.
419: *
420: * @param nodeName the name of the new node
421: * @param path the file/directory to use when setting attributes
422: * @throws SAXException if an error occurs while creating the node
423: */
424: protected void startNode(String nodeName, File path)
425: throws SAXException {
426: if (this .validity != null) {
427: this .validity.addFile(path);
428: }
429: setNodeAttributes(path);
430: super .contentHandler.startElement(URI, nodeName, PREFIX + ':'
431: + nodeName, attributes);
432: }
433:
434: /**
435: * Sets the attributes for a given path. The default method sets attributes
436: * for the name of thefile/directory and for the last modification time
437: * of the path.
438: *
439: * @param path the file/directory to use when setting attributes
440: * @throws SAXException if an error occurs while setting the attributes
441: */
442: protected void setNodeAttributes(File path) throws SAXException {
443: long lastModified = path.lastModified();
444: attributes.clear();
445: attributes.addAttribute("", FILENAME_ATTR_NAME,
446: FILENAME_ATTR_NAME, "CDATA", path.getName());
447: attributes.addAttribute("", LASTMOD_ATTR_NAME,
448: LASTMOD_ATTR_NAME, "CDATA", Long.toString(path
449: .lastModified()));
450: attributes.addAttribute("", DATE_ATTR_NAME, DATE_ATTR_NAME,
451: "CDATA", dateFormatter.format(new Date(lastModified)));
452: attributes.addAttribute("", SIZE_ATTR_NAME, SIZE_ATTR_NAME,
453: "CDATA", Long.toString(path.length()));
454: if (this .isRequestedDirectory) {
455: attributes.addAttribute("", "sort", "sort", "CDATA",
456: this .sort);
457: attributes.addAttribute("", "reverse", "reverse", "CDATA",
458: String.valueOf(this .reverse));
459: attributes.addAttribute("", "requested", "requested",
460: "CDATA", "true");
461: this .isRequestedDirectory = false;
462: }
463: }
464:
465: /**
466: * Ends the named node.
467: *
468: * @param nodeName the name of the new node
469: * @throws SAXException if an error occurs while closing the node
470: */
471: protected void endNode(String nodeName) throws SAXException {
472: super .contentHandler.endElement(URI, nodeName, PREFIX + ':'
473: + nodeName);
474: }
475:
476: /**
477: * Determines if a given File is the defined root.
478: *
479: * @param path the File to check
480: * @return true if the File is the root or the root pattern is not set,
481: * false otherwise.
482: */
483: protected boolean isRoot(File path) {
484: return (this .rootRE == null) ? true : this .rootRE.match(path
485: .getName());
486: }
487:
488: /**
489: * Determines if a given File shall be visible.
490: *
491: * @param path the File to check
492: * @return true if the File shall be visible or the include Pattern is <code>null</code>,
493: * false otherwise.
494: */
495: protected boolean isIncluded(File path) {
496: return (this .includeRE == null) ? true : this .includeRE
497: .match(path.getName());
498: }
499:
500: /**
501: * Determines if a given File shall be excluded from viewing.
502: *
503: * @param path the File to check
504: * @return false if the given File shall not be excluded or the exclude Pattern is <code>null</code>,
505: * true otherwise.
506: */
507: protected boolean isExcluded(File path) {
508: return (this .excludeRE == null) ? false : this .excludeRE
509: .match(path.getName());
510: }
511:
512: /**
513: * Recycle resources
514: */
515: public void recycle() {
516: if (this .resolver != null) {
517: this .resolver.release(this .directorySource);
518: this .directorySource = null;
519: }
520: this .cacheKeyParList = null;
521: this .attributes = null;
522: this .dateFormatter = null;
523: this .rootRE = null;
524: this .includeRE = null;
525: this .excludeRE = null;
526: this .validity = null;
527: super .recycle();
528: }
529:
530: /** Specific validity class, that holds all files that have been generated */
531: public static class DirValidity implements SourceValidity {
532:
533: private long expiry;
534: private long delay;
535: List files = new ArrayList();
536: List fileDates = new ArrayList();
537:
538: public DirValidity(long delay) {
539: expiry = System.currentTimeMillis() + delay;
540: this .delay = delay;
541: }
542:
543: public int isValid() {
544: if (System.currentTimeMillis() <= expiry) {
545: return SourceValidity.VALID;
546: }
547:
548: int len = files.size();
549: for (int i = 0; i < len; i++) {
550: File f = (File) files.get(i);
551: if (!f.exists()) {
552: return SourceValidity.INVALID; // File was removed
553: }
554:
555: long oldDate = ((Long) fileDates.get(i)).longValue();
556: long newDate = f.lastModified();
557:
558: if (oldDate != newDate) {
559: // File's last modified date has changed since last check
560: // NOTE: this occurs on directories as well when a file is added
561: return SourceValidity.INVALID;
562: }
563: }
564:
565: // all content is up to date: update the expiry date
566: expiry = System.currentTimeMillis() + delay;
567: return SourceValidity.VALID;
568: }
569:
570: public int isValid(SourceValidity newValidity) {
571: return isValid();
572: }
573:
574: public void addFile(File f) {
575: files.add(f);
576: fileDates.add(new Long(f.lastModified()));
577: }
578: }
579: }
|