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.components.source.impl.MultiSourceValidity;
025: import org.apache.cocoon.environment.SourceResolver;
026: import org.apache.excalibur.source.Source;
027: import org.apache.excalibur.source.SourceException;
028: import org.apache.excalibur.source.SourceValidity;
029: import org.apache.excalibur.source.TraversableSource;
030: import org.apache.regexp.RE;
031: import org.apache.regexp.RESyntaxException;
032: import org.xml.sax.SAXException;
033: import org.xml.sax.helpers.AttributesImpl;
034:
035: import java.io.IOException;
036: import java.io.Serializable;
037: import java.text.SimpleDateFormat;
038: import java.util.Locale;
039: import java.util.ArrayList;
040: import java.util.Collection;
041: import java.util.Date;
042: import java.util.Enumeration;
043: import java.util.Iterator;
044: import java.util.List;
045: import java.util.Map;
046: import java.util.Stack;
047: import java.util.Arrays;
048: import java.util.Comparator;
049: import java.util.TimeZone;
050:
051: /**
052: * Generates an XML source hierarchy listing from a Traversable Source.
053: * <p>
054: * The root node of the generated document will normally be a
055: * <code>collection</code> node and a collection node can contain zero or more
056: * <code>resource</code> or collection nodes. A resource node has no children.
057: * Each node will contain the following attributes:
058: * <blockquote>
059: * <dl>
060: * <dt> name
061: * <dd> the name of the source
062: * <dt> lastModified
063: * <dd> the time the source was last modified, measured as the number of
064: * milliseconds since the epoch (as in java.io.File.lastModified)
065: * <dt> size
066: * <dd> the source size, in bytes (as in java.io.File.length)
067: * <dt> date (optional)
068: * <dd> the time the source was last modified in human-readable form
069: * </dl>
070: * </blockquote>
071: * <p>
072: * <b>Configuration options:</b>
073: * <dl>
074: * <dt> <i>depth</i> (optional)
075: * <dd> Sets how deep TraversableGenerator should delve into the
076: * source hierarchy. If set to 1 (the default), only the starting
077: * collection's immediate contents will be returned.
078: * <dt> <i>sort</i> (optional)
079: * <dd> Sort order in which the nodes are returned. Possible values are
080: * name, size, time, collection. collection is the same as name,
081: * except that the collection entries are listed first. System order is
082: * default.
083: * <dt> <i>reverse</i> (optional)
084: * <dd> Reverse the order of the sort
085: * <dt> <i>dateFormat</i> (optional)
086: * <dd> Sets the format for the date attribute of each node, as
087: * described in java.text.SimpleDateFormat. If unset, the default
088: * format for the current locale will be used.
089: * <dt> <i>timeZone</i> (optional)
090: * <dd> Sets the time zone offset ID for the date attribute, as
091: * described in java.util.TimeZone. If unset, the default
092: * system time zone will be used.
093: * <dt> <i>refreshDelay</i> (optional)
094: * <dd> Sets the delay (in seconds) between checks on the source hierarchy
095: * for changed content. Defaults to 1 second.
096: * </dl>
097: * </p>
098: *
099: * @author <a href="mailto:pier@apache.org">Pierpaolo Fumagalli</a>
100: * (Apache Software Foundation)
101: * @author <a href="mailto:conny@smb-tec.com">Conny Krappatsch</a>
102: * (SMB GmbH) for Virbus AG
103: * @author <a href="d.madama@pro-netics.com">Daniele Madama</a>
104: * @author <a href="gianugo@apache.org">Gianugo Rabellino</a>
105: * @version CVS $Id: TraversableGenerator.java 433543 2006-08-22 06:22:54Z crossley $
106: */
107: public class TraversableGenerator extends ServiceableGenerator
108: implements CacheableProcessingComponent {
109:
110: /** The URI of the namespace of this generator. */
111: protected static final String URI = "http://apache.org/cocoon/collection/1.0";
112:
113: /** The namespace prefix for this namespace. */
114: protected static final String PREFIX = "collection";
115:
116: /* Node and attribute names */
117: protected static final String COL_NODE_NAME = "collection";
118: protected static final String RESOURCE_NODE_NAME = "resource";
119:
120: protected static final String RES_NAME_ATTR_NAME = "name";
121: protected static final String URI_ATTR_NAME = "uri";
122: protected static final String LASTMOD_ATTR_NAME = "lastModified";
123: protected static final String DATE_ATTR_NAME = "date";
124: protected static final String SIZE_ATTR_NAME = "size";
125:
126: /** The validity that is being built */
127: protected MultiSourceValidity validity;
128:
129: /**
130: * Convenience object, so we don't need to create an AttributesImpl for every element.
131: */
132: protected AttributesImpl attributes;
133:
134: /**
135: * The cache key needs to be generated for the configuration of this
136: * generator, so storing the parameters for generateKey().
137: * Using the member variables after setup() would not work I guess. I don't
138: * know a way from the regular expressions back to the pattern or at least
139: * a useful string.
140: */
141: protected List cacheKeyParList;
142:
143: /**
144: * The depth parameter determines how deep the TraversableGenerator should delve.
145: */
146: protected int depth;
147:
148: /**
149: * The dateFormatter determines into which date format the lastModified
150: * time should be converted.
151: * FIXME: SimpleDateFormat is not supported by all locales!
152: */
153: protected SimpleDateFormat dateFormatter;
154:
155: /** The delay between checks on updates to the source hierarchy. */
156: protected long refreshDelay;
157:
158: /**
159: * The sort parameter determines by which attribute the content of one
160: * collection should be sorted. Possible values are "name", "size", "time"
161: * and "collection", where "collection" is the same as "name", except that
162: * collection entries are listed first.
163: */
164: protected String sort;
165:
166: /** The reverse parameter reverses the sort order. <code>false</code> is default. */
167: protected boolean reverse;
168:
169: /** The regular expression for the root pattern. */
170: protected RE rootRE;
171:
172: /** The regular expression for the include pattern. */
173: protected RE includeRE;
174:
175: /** The regular expression for the exclude pattern. */
176: protected RE excludeRE;
177:
178: /**
179: * This is only set to true for the requested source specified by the
180: * <code>src</code> attribute on the generator's configuration.
181: */
182: protected boolean isRequestedSource;
183:
184: /**
185: * Set the request parameters. Must be called before the generate method.
186: *
187: * @param resolver the SourceResolver object
188: * @param objectModel a <code>Map</code> containing model object
189: * @param src the Traversable Source to be XMLized specified as
190: * <code>src</code> attribute on <map:generate/>
191: * @param par configuration parameters
192: */
193: public void setup(SourceResolver resolver, Map objectModel,
194: String src, Parameters par) throws ProcessingException,
195: SAXException, IOException {
196: if (src == null) {
197: throw new ProcessingException(
198: "No src attribute pointing to a traversable source to be XMLized specified.");
199: }
200: super .setup(resolver, objectModel, src, par);
201:
202: this .cacheKeyParList = new ArrayList();
203: this .cacheKeyParList.add(src);
204:
205: this .depth = par.getParameterAsInteger("depth", 1);
206: this .cacheKeyParList.add(String.valueOf(this .depth));
207:
208: String dateFormatString = par.getParameter("dateFormat", null);
209: this .cacheKeyParList.add(dateFormatString);
210: if (dateFormatString != null) {
211: String locale = par.getParameter("locale", null);
212: if (locale != null) {
213: this .dateFormatter = new SimpleDateFormat(
214: dateFormatString, new Locale(locale, ""));
215: } else {
216: this .dateFormatter = new SimpleDateFormat(
217: dateFormatString);
218: }
219: } else {
220: this .dateFormatter = new SimpleDateFormat();
221: }
222:
223: String timeZone = par.getParameter("timeZone", null);
224: if (timeZone != null) {
225: this .dateFormatter.setTimeZone(TimeZone
226: .getTimeZone(timeZone));
227: }
228:
229: this .sort = par.getParameter("sort", "name");
230: this .cacheKeyParList.add(this .sort);
231:
232: this .reverse = par.getParameterAsBoolean("reverse", false);
233: this .cacheKeyParList.add(String.valueOf(this .reverse));
234:
235: this .refreshDelay = par.getParameterAsLong("refreshDelay", 1L) * 1000L;
236: this .cacheKeyParList.add(String.valueOf(this .refreshDelay));
237:
238: if (this .getLogger().isDebugEnabled()) {
239: this .getLogger().debug("depth: " + this .depth);
240: this .getLogger().debug(
241: "dateFormat: " + this .dateFormatter.toPattern());
242: this .getLogger().debug("timeZone: " + timeZone);
243: this .getLogger().debug("sort: " + this .sort);
244: this .getLogger().debug("reverse: " + this .reverse);
245: this .getLogger()
246: .debug("refreshDelay: " + this .refreshDelay);
247: }
248:
249: String rePattern = null;
250: try {
251: rePattern = par.getParameter("root", null);
252: if (this .getLogger().isDebugEnabled()) {
253: this .getLogger().debug("root pattern: " + rePattern);
254: }
255: this .cacheKeyParList.add(rePattern);
256: this .rootRE = (rePattern == null) ? null
257: : new RE(rePattern);
258:
259: rePattern = par.getParameter("include", null);
260: if (this .getLogger().isDebugEnabled()) {
261: this .getLogger().debug("include pattern: " + rePattern);
262: }
263: this .cacheKeyParList.add(rePattern);
264: this .includeRE = (rePattern == null) ? null : new RE(
265: rePattern);
266:
267: rePattern = par.getParameter("exclude", null);
268: if (this .getLogger().isDebugEnabled()) {
269: this .getLogger().debug("exclude pattern: " + rePattern);
270: }
271: this .cacheKeyParList.add(rePattern);
272: this .excludeRE = (rePattern == null) ? null : new RE(
273: rePattern);
274:
275: } catch (RESyntaxException rese) {
276: throw new ProcessingException(
277: "Syntax error in regexp pattern '" + rePattern
278: + "'", rese);
279: }
280:
281: this .isRequestedSource = false;
282: this .attributes = new AttributesImpl();
283: }
284:
285: /* (non-Javadoc)
286: * @see org.apache.cocoon.caching.CacheableProcessingComponent#getKey()
287: */
288: public Serializable getKey() {
289: StringBuffer buffer = new StringBuffer();
290: int len = this .cacheKeyParList.size();
291: for (int i = 0; i < len; i++) {
292: buffer.append(this .cacheKeyParList.get(i));
293: buffer.append(':');
294: }
295: return buffer.toString();
296: }
297:
298: /**
299: * Gets the source validity, using a deferred validity object. The validity
300: * is initially empty since the resources that define it are not known
301: * before generation has occured. So the returned object is kept by the
302: * generator and filled with each of the resources that is traversed.
303: *
304: * @see org.apache.cocoon.components.source.impl.MultiSourceValidity
305: */
306: public SourceValidity getValidity() {
307: if (this .validity == null) {
308: this .validity = new MultiSourceValidity(this .resolver,
309: this .refreshDelay);
310: }
311: return this .validity;
312: }
313:
314: /**
315: * Generate XML data.
316: *
317: * @throws SAXException if an error occurs while outputting the document
318: * @throws ProcessingException if something went wrong while traversing
319: * the source hierarchy
320: */
321: public void generate() throws SAXException, ProcessingException {
322: Source src = null;
323: Stack ancestors = null;
324: try {
325: src = this .resolver.resolveURI(this .source);
326: if (!(src instanceof TraversableSource)) {
327: throw new SourceException(this .source
328: + " is not a traversable source");
329: }
330: final TraversableSource inputSource = (TraversableSource) src;
331:
332: if (!inputSource.exists()) {
333: throw new ResourceNotFoundException(this .source
334: + " does not exist.");
335: }
336:
337: this .contentHandler.startDocument();
338: this .contentHandler.startPrefixMapping(PREFIX, URI);
339:
340: ancestors = getAncestors(inputSource);
341: addAncestorPath(inputSource, ancestors);
342:
343: this .contentHandler.endPrefixMapping(PREFIX);
344: this .contentHandler.endDocument();
345: if (this .validity != null) {
346: this .validity.close();
347: }
348: } catch (SourceException se) {
349: throw SourceUtil.handle(se);
350: } catch (IOException ioe) {
351: throw new ResourceNotFoundException(
352: "Could not read collection " + this .source, ioe);
353: } finally {
354: if (src != null) {
355: this .resolver.release(src);
356: }
357: if (ancestors != null) {
358: Enumeration enumeration = ancestors.elements();
359: while (enumeration.hasMoreElements()) {
360: resolver
361: .release((Source) enumeration.nextElement());
362: }
363: }
364: }
365: }
366:
367: /**
368: * Creates a stack containing the ancestors of a traversable source up to
369: * specific parent matching the root pattern.
370: *
371: * @param source the traversable source whose ancestors shall be retrieved
372: * @return a Stack containing the ancestors.
373: */
374: protected Stack getAncestors(TraversableSource source)
375: throws IOException {
376: TraversableSource parent = source;
377: Stack ancestors = new Stack();
378:
379: while ((parent != null) && !isRoot(parent)) {
380: parent = (TraversableSource) parent.getParent();
381: if (parent != null) {
382: ancestors.push(parent);
383: } else {
384: // no ancestor matched the root pattern
385: ancestors.clear();
386: }
387: }
388:
389: return ancestors;
390: }
391:
392: /**
393: * Adds recursively the path from the source matched by the root pattern
394: * down to the requested source.
395: *
396: * @param source the requested source.
397: * @param ancestors the stack of the ancestors.
398: * @throws SAXException
399: * @throws ProcessingException
400: */
401: protected void addAncestorPath(TraversableSource source,
402: Stack ancestors) throws SAXException, ProcessingException {
403: if (ancestors.empty()) {
404: this .isRequestedSource = true;
405: addPath(source, depth);
406: } else {
407: startNode(COL_NODE_NAME, (TraversableSource) ancestors
408: .pop());
409: addAncestorPath(source, ancestors);
410: endNode(COL_NODE_NAME);
411: }
412: }
413:
414: /**
415: * Adds a single node to the generated document. If the path is a
416: * collection and depth is greater than zero, then recursive calls
417: * are made to add nodes for the collection's children.
418: *
419: * @param source the resource/collection to process
420: * @param depth how deep to scan the collection hierarchy
421: *
422: * @throws SAXException if an error occurs while constructing nodes
423: * @throws ProcessingException if a problem occurs with the source
424: */
425: protected void addPath(TraversableSource source, int depth)
426: throws SAXException, ProcessingException {
427: if (source.isCollection()) {
428: startNode(COL_NODE_NAME, source);
429: addContent(source);
430: if (depth > 0) {
431:
432: Collection contents = null;
433:
434: try {
435: contents = source.getChildren();
436: if (sort.equals("name")) {
437: Arrays.sort(contents.toArray(),
438: new Comparator() {
439: public int compare(Object o1,
440: Object o2) {
441: if (reverse) {
442: return ((TraversableSource) o2)
443: .getName()
444: .compareTo(
445: ((TraversableSource) o1)
446: .getName());
447: }
448: return ((TraversableSource) o1)
449: .getName()
450: .compareTo(
451: ((TraversableSource) o2)
452: .getName());
453: }
454: });
455: } else if (sort.equals("size")) {
456: Arrays.sort(contents.toArray(),
457: new Comparator() {
458: public int compare(Object o1,
459: Object o2) {
460: if (reverse) {
461: return new Long(
462: ((TraversableSource) o2)
463: .getContentLength())
464: .compareTo(new Long(
465: ((TraversableSource) o1)
466: .getContentLength()));
467: }
468: return new Long(
469: ((TraversableSource) o1)
470: .getContentLength())
471: .compareTo(new Long(
472: ((TraversableSource) o2)
473: .getContentLength()));
474: }
475: });
476: } else if (sort.equals("lastmodified")) {
477: Arrays.sort(contents.toArray(),
478: new Comparator() {
479: public int compare(Object o1,
480: Object o2) {
481: if (reverse) {
482: return new Long(
483: ((TraversableSource) o2)
484: .getLastModified())
485: .compareTo(new Long(
486: ((TraversableSource) o1)
487: .getLastModified()));
488: }
489: return new Long(
490: ((TraversableSource) o1)
491: .getLastModified())
492: .compareTo(new Long(
493: ((TraversableSource) o2)
494: .getLastModified()));
495: }
496: });
497: } else if (sort.equals("collection")) {
498: Arrays.sort(contents.toArray(),
499: new Comparator() {
500: public int compare(Object o1,
501: Object o2) {
502: TraversableSource ts1 = (TraversableSource) o1;
503: TraversableSource ts2 = (TraversableSource) o2;
504:
505: if (reverse) {
506: if (ts2.isCollection()
507: && !ts1
508: .isCollection())
509: return -1;
510: if (!ts2.isCollection()
511: && ts1
512: .isCollection())
513: return 1;
514: return ts2
515: .getName()
516: .compareTo(
517: ts1
518: .getName());
519: }
520: if (ts2.isCollection()
521: && !ts1.isCollection())
522: return 1;
523: if (!ts2.isCollection()
524: && ts1.isCollection())
525: return -1;
526: return ts1.getName().compareTo(
527: ts2.getName());
528: }
529: });
530: }
531:
532: for (int i = 0; i < contents.size(); i++) {
533: if (isIncluded((TraversableSource) contents
534: .toArray()[i])
535: && !isExcluded((TraversableSource) contents
536: .toArray()[i])) {
537: addPath((TraversableSource) contents
538: .toArray()[i], depth - 1);
539: }
540: }
541: } catch (SourceException e) {
542: throw new ProcessingException("Error adding paths",
543: e);
544: } finally {
545: if (contents != null) {
546: Iterator iter = contents.iterator();
547: while (iter.hasNext()) {
548: resolver.release((Source) iter.next());
549: }
550: }
551: }
552: }
553: endNode(COL_NODE_NAME);
554: } else {
555: if (isIncluded(source) && !isExcluded(source)) {
556: startNode(RESOURCE_NODE_NAME, source);
557: addContent(source);
558: endNode(RESOURCE_NODE_NAME);
559: }
560: }
561: }
562:
563: /**
564: * Allow subclasses a chance to generate additional elements within collection and resource
565: * elements.
566: *
567: * @param source the source to generate additional data for.
568: */
569: protected void addContent(TraversableSource source)
570: throws SAXException, ProcessingException {
571: }
572:
573: /**
574: * Begins a named node and calls setNodeAttributes to set its attributes.
575: *
576: * @param nodeName the name of the new node
577: * @param source the source a node with its attributes is added for
578: *
579: * @throws SAXException if an error occurs while creating the node
580: */
581: protected void startNode(String nodeName, TraversableSource source)
582: throws SAXException, ProcessingException {
583: if (this .validity != null) {
584: this .validity.addSource(source);
585: }
586: setNodeAttributes(source);
587: super .contentHandler.startElement(URI, nodeName, PREFIX + ':'
588: + nodeName, attributes);
589: }
590:
591: /**
592: * Sets the attributes for a given source. For example attributes for the
593: * name, the size and the last modification date of the source are added.
594: *
595: * @param source the source attributes are added for
596: */
597: protected void setNodeAttributes(TraversableSource source)
598: throws SAXException, ProcessingException {
599: long lastModified = source.getLastModified();
600: attributes.clear();
601: attributes.addAttribute("", RES_NAME_ATTR_NAME,
602: RES_NAME_ATTR_NAME, "CDATA", source.getName());
603: attributes.addAttribute("", URI_ATTR_NAME, URI_ATTR_NAME,
604: "CDATA", source.getURI());
605: attributes.addAttribute("", LASTMOD_ATTR_NAME,
606: LASTMOD_ATTR_NAME, "CDATA", Long.toString(source
607: .getLastModified()));
608: attributes.addAttribute("", DATE_ATTR_NAME, DATE_ATTR_NAME,
609: "CDATA", dateFormatter.format(new Date(lastModified)));
610: attributes.addAttribute("", SIZE_ATTR_NAME, SIZE_ATTR_NAME,
611: "CDATA", Long.toString(source.getContentLength()));
612: if (this .isRequestedSource) {
613: attributes.addAttribute("", "sort", "sort", "CDATA",
614: this .sort);
615: attributes.addAttribute("", "reverse", "reverse", "CDATA",
616: String.valueOf(this .reverse));
617: attributes.addAttribute("", "requested", "requested",
618: "CDATA", "true");
619: this .isRequestedSource = false;
620: }
621: }
622:
623: /**
624: * Ends the named node.
625: *
626: * @param nodeName the name of the new node
627: *
628: * @throws SAXException if an error occurs while closing the node
629: */
630: protected void endNode(String nodeName) throws SAXException {
631: super .contentHandler.endElement(URI, nodeName, PREFIX + ':'
632: + nodeName);
633: }
634:
635: /**
636: * Determines if a given source is the defined root.
637: *
638: * @param source the source to check
639: *
640: * @return true if the source is the root or the root pattern is not set,
641: * false otherwise.
642: */
643: protected boolean isRoot(TraversableSource source) {
644: return this .rootRE == null ? true : this .rootRE.match(source
645: .getName());
646: }
647:
648: /**
649: * Determines if a given source shall be visible.
650: *
651: * @param source the source to check
652: *
653: * @return true if the source shall be visible or the include Pattern is not set,
654: * false otherwise.
655: */
656: protected boolean isIncluded(TraversableSource source) {
657: return this .includeRE == null ? true : this .includeRE
658: .match(source.getName());
659: }
660:
661: /**
662: * Determines if a given source shall be excluded from viewing.
663: *
664: * @param source the source to check
665: *
666: * @return false if the given source shall not be excluded or the exclude Pattern is not set,
667: * true otherwise.
668: */
669: protected boolean isExcluded(TraversableSource source) {
670: return this .excludeRE == null ? false : this .excludeRE
671: .match(source.getName());
672: }
673:
674: /**
675: * Recycle resources
676: */
677: public void recycle() {
678: this.cacheKeyParList = null;
679: this.attributes = null;
680: this.dateFormatter = null;
681: this.rootRE = null;
682: this.includeRE = null;
683: this.excludeRE = null;
684: this.validity = null;
685: super.recycle();
686: }
687: }
|