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.transformation.pagination;
018:
019: import org.apache.avalon.framework.activity.Disposable;
020: import org.apache.avalon.framework.parameters.Parameters;
021: import org.apache.avalon.framework.service.ServiceException;
022: import org.apache.avalon.framework.service.ServiceManager;
023: import org.apache.avalon.framework.service.Serviceable;
024: import org.apache.cocoon.ProcessingException;
025: import org.apache.cocoon.caching.CacheableProcessingComponent;
026: import org.apache.cocoon.environment.ObjectModelHelper;
027: import org.apache.cocoon.environment.Request;
028: import org.apache.cocoon.environment.SourceResolver;
029: import org.apache.cocoon.transformation.AbstractTransformer;
030: import org.apache.excalibur.source.Source;
031: import org.apache.excalibur.source.SourceException;
032: import org.apache.excalibur.source.SourceValidity;
033: import org.apache.excalibur.source.impl.validity.AggregatedValidity;
034: import org.apache.excalibur.source.impl.validity.TimeStampValidity;
035: import org.apache.excalibur.store.Store;
036: import org.apache.excalibur.xml.sax.SAXParser;
037: import org.xml.sax.Attributes;
038: import org.xml.sax.InputSource;
039: import org.xml.sax.SAXException;
040: import org.xml.sax.helpers.AttributesImpl;
041:
042: import java.io.IOException;
043: import java.io.Serializable;
044: import java.util.Map;
045:
046: /**
047: * A paginating transformer.
048: *
049: * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
050: * @author <a href="mailto:stephan@apache.org">Stephan Michels</a>
051: * @author <a href="mailto:bhtek@yahoo.com">Boon Hian Tek</a>
052: * @version CVS $Id: Paginator.java 433543 2006-08-22 06:22:54Z crossley $
053: */
054: public class Paginator extends AbstractTransformer implements
055: Serviceable, Disposable, CacheableProcessingComponent {
056:
057: public static final String PAGINATE_URI = "http://apache.org/cocoon/paginate/1.0";
058: public static final String PAGINATE_PREFIX = "page";
059: public static final String PAGINATE_PREFIX_TOKEN = PAGINATE_PREFIX
060: + ":";
061:
062: private ServiceManager manager;
063: private SAXParser parser;
064: private Store store;
065: private SourceResolver resolver;
066: private Source inputSource;
067: private int page;
068: private int item;
069: private String itemGroup;
070: private String requestURI;
071: private Request request;
072: private Pagesheet pagesheet;
073: private int level;
074: private boolean prefixMapping;
075:
076: /**
077: * Set the current <code>ServiceManager</code> instance used by this
078: * <code>Serviceable</code>.
079: *
080: * @param manager Description of the Parameter
081: */
082: public void service(ServiceManager manager) throws ServiceException {
083: try {
084: this .manager = manager;
085: getLogger().debug("Looking up " + SAXParser.ROLE);
086: this .parser = (SAXParser) manager.lookup(SAXParser.ROLE);
087:
088: getLogger().debug("Looking up " + Store.TRANSIENT_STORE);
089: this .store = (Store) manager.lookup(Store.TRANSIENT_STORE);
090: } catch (Exception e) {
091: getLogger().error("Could not find component", e);
092: }
093: }
094:
095: /**
096: * Dispose this component.
097: */
098: public void dispose() {
099: if (this .parser != null) {
100: this .manager.release(this .parser);
101: } else {
102: this .parser = null;
103: }
104: if (this .store != null) {
105: this .manager.release(this .store);
106: } else {
107: this .store = null;
108: }
109: }
110:
111: /**
112: * Setup the transformer.
113: */
114: public void setup(SourceResolver resolver, Map objectModel,
115: String src, Parameters par) throws ProcessingException,
116: SAXException, IOException {
117:
118: if (src == null) {
119: throw new ProcessingException(
120: "I need the paginate instructions (pagesheet) to continue. Set the 'src' attribute.");
121: }
122:
123: try {
124: this .level = 0;
125: this .prefixMapping = false;
126: this .resolver = resolver;
127: this .inputSource = resolver.resolveURI(src);
128: if (getLogger().isDebugEnabled()) {
129: getLogger().debug(
130: "Using pagesheet: '"
131: + this .inputSource.getURI() + "' in "
132: + this + ", last modified: "
133: + this .inputSource.getLastModified());
134: }
135: this .page = par.getParameterAsInteger("page", 1);
136: this .item = par.getParameterAsInteger("item", 0);
137: this .itemGroup = par.getParameter("item-group", "");
138: if (getLogger().isDebugEnabled()) {
139: getLogger().debug(
140: "Paginating with [page = " + this .page
141: + ", item = " + this .item
142: + ", item-group = " + this .itemGroup
143: + "]");
144: }
145:
146: this .request = ObjectModelHelper.getRequest(objectModel);
147: this .requestURI = request.getRequestURI();
148:
149: // Get the pagesheet factory from the Store if available,
150: // otherwise load it and put it into the store for further request
151: if (store != null) {
152: pagesheet = (Pagesheet) store.get(src);
153: }
154:
155: // If not in the store or if pagesheet has changed, loads and stores it
156: if ((pagesheet == null)
157: || pagesheet.modifiedSince(inputSource
158: .getLastModified())) {
159: pagesheet = new Pagesheet();
160: pagesheet
161: .setLastModified(inputSource.getLastModified());
162: parser.parse(new InputSource(inputSource
163: .getInputStream()), pagesheet);
164: if (store != null) {
165: store.store(src, pagesheet);
166: }
167: }
168:
169: // Clone it in order to avoid concurrency collisions since the
170: // implementation is not reentrant.
171: this .pagesheet = (Pagesheet) this .pagesheet.clone();
172: } catch (SourceException se) {
173: throw new ProcessingException("Could not retrieve source '"
174: + src + "'", se);
175: }
176: }
177:
178: public void recycle() {
179: if (null != this .inputSource) {
180: this .resolver.release(this .inputSource);
181: this .inputSource = null;
182: }
183: this .resolver = null;
184: super .recycle();
185: }
186:
187: /**
188: * Generate the unique key. This key must be unique inside the space of
189: * this component. This method must be invoked before the
190: * generateValidity() method.
191: *
192: * @return The generated key or <code>null</code> if the component is
193: * currently not cacheable.
194: */
195: public Serializable getKey() {
196: if (this .inputSource.getLastModified() != 0) {
197: return this .inputSource.getURI() + page;
198: } else {
199: return null;
200: }
201: }
202:
203: /**
204: * Generate the validity object. Before this method can be invoked the
205: * generateKey() method must be invoked.
206: *
207: * @return The generated validity object or <code>null</code> if the
208: * component is currently not cacheable.
209: */
210: public SourceValidity getValidity() {
211: if (this .inputSource.getLastModified() != 0) {
212: AggregatedValidity validity = new AggregatedValidity();
213:
214: validity.add(new TimeStampValidity(page));
215: validity.add(this .inputSource.getValidity());
216: return validity;
217: } else {
218: return null;
219: }
220: }
221:
222: /**
223: * Receive notification of the beginning of an element.
224: *
225: * @param uri The Namespace URI, or the empty string if the
226: * element has no Namespace URI or if Namespace processing is not being
227: * performed.
228: * @param loc The local name (without prefix), or the empty
229: * string if Namespace processing is not being performed.
230: * @param raw The raw XML 1.0 name (with prefix), or the empty
231: * string if raw names are not available.
232: * @param a The attributes attached to the element. If there
233: * are no attributes, it shall be an empty Attributes object.
234: */
235: public void startElement(String uri, String loc, String raw,
236: Attributes a) throws SAXException {
237: if (!prefixMapping) {
238: super .startPrefixMapping(PAGINATE_PREFIX, PAGINATE_URI);
239: this .prefixMapping = true;
240: }
241: level++;
242: pagesheet.processStartElement(uri, loc);
243: if (pagesheet.isInPage(page, item, itemGroup)) {
244: int itemCount = pagesheet.itemCount(uri, loc);
245:
246: if (itemCount > 0) {
247: String itemGroup = pagesheet.getItemGroupName(uri, loc);
248: AttributesImpl atts = new AttributesImpl(a);
249:
250: atts.addAttribute(PAGINATE_URI, "item",
251: PAGINATE_PREFIX_TOKEN + "item", "CDATA", String
252: .valueOf(itemCount));
253: atts.addAttribute(PAGINATE_URI, "item-group",
254: PAGINATE_PREFIX_TOKEN + "item-group", "CDATA",
255: itemGroup);
256: super .startElement(uri, loc, raw, atts);
257: } else {
258: super .startElement(uri, loc, raw, a);
259: }
260: }
261: }
262:
263: /**
264: * Receive notification of the end of an element.
265: *
266: * @param uri The Namespace URI, or the empty string if the
267: * element has no Namespace URI or if Namespace processing is not being
268: * performed.
269: * @param loc The local name (without prefix), or the empty
270: * string if Namespace processing is not being performed.
271: * @param raw The raw XML 1.0 name (with prefix), or the empty
272: * string if raw names are not available.
273: */
274: public void endElement(String uri, String loc, String raw)
275: throws SAXException {
276: level--;
277:
278: // Prevent infinite recursive loop.
279: if (PAGINATE_URI.equals(uri)) {
280: super .endElement(uri, loc, raw);
281: return;
282: }
283:
284: if (pagesheet.isInPage(page, item, itemGroup)) {
285: if (level == 0) {
286: if (item == 0) {
287: int totalPages = pagesheet.getTotalPages();
288: PageRules rules = pagesheet.getPageRules(page);
289:
290: Integer[] rangeLinks = rules.getRangeLinks();
291: int unitLinks = rules.unitLinks;
292: int currentPage = page;
293:
294: // call add paginate
295: addPaginateTags(rangeLinks, unitLinks, currentPage,
296: totalPages, requestURI, this );
297:
298: } else {
299: int totalItems = pagesheet.getTotalItems(itemGroup);
300: AttributesImpl atts = new AttributesImpl();
301:
302: atts.addAttribute("", "current", "current",
303: "CDATA", String.valueOf(item));
304: atts.addAttribute("", "total", "total", "CDATA",
305: String.valueOf(totalItems));
306: atts.addAttribute("", "current-uri", "current-uri",
307: "CDATA", requestURI);
308: atts.addAttribute("", "clean-uri", "clean-uri",
309: "CDATA", cleanURI(requestURI, item));
310: atts.addAttribute("", "page", "page", "CDATA",
311: String.valueOf(pagesheet.getPageForItem(
312: item, itemGroup)));
313: super .startElement(PAGINATE_URI, "item",
314: PAGINATE_PREFIX_TOKEN + "item", atts);
315: if (item > 1) {
316: atts.clear();
317: atts.addAttribute("", "type", "type", "CDATA",
318: "prev");
319: atts.addAttribute("", "uri", "uri", "CDATA",
320: encodeURI(requestURI, item, item - 1));
321: super .startElement(PAGINATE_URI, "link",
322: PAGINATE_PREFIX_TOKEN + "link", atts);
323: super .endElement(PAGINATE_URI, "link",
324: PAGINATE_PREFIX_TOKEN + "link");
325: }
326: if (item <= totalItems) {
327: atts.clear();
328: atts.addAttribute("", "type", "type", "CDATA",
329: "next");
330: atts.addAttribute("", "uri", "uri", "CDATA",
331: encodeURI(requestURI, item, item + 1));
332: super .startElement(PAGINATE_URI, "link",
333: PAGINATE_PREFIX_TOKEN + "link", atts);
334: super .endElement(PAGINATE_URI, "link",
335: PAGINATE_PREFIX_TOKEN + "link");
336: }
337: super .endElement(PAGINATE_URI, "item",
338: PAGINATE_PREFIX_TOKEN + "item");
339: }
340:
341: super .endPrefixMapping(PAGINATE_PREFIX);
342: }
343:
344: super .endElement(uri, loc, raw);
345: }
346:
347: pagesheet.processEndElement(uri, loc);
348: }
349:
350: public static void addPaginateTags(Integer[] rangeLinks,
351: int unitLinks, int currentPage, int totalPages,
352: String requestURI, AbstractTransformer saxTransformer)
353: throws SAXException {
354: AttributesImpl atts = new AttributesImpl();
355:
356: atts.addAttribute("", "current", "current", "CDATA", String
357: .valueOf(currentPage));
358: atts.addAttribute("", "total", "total", "CDATA", String
359: .valueOf(totalPages));
360: atts.addAttribute("", "current-uri", "current-uri", "CDATA",
361: requestURI);
362: atts.addAttribute("", "clean-uri", "clean-uri", "CDATA",
363: Paginator.cleanURI(requestURI, currentPage));
364: saxTransformer.startElement(Paginator.PAGINATE_URI, "page",
365: Paginator.PAGINATE_PREFIX_TOKEN + "page", atts);
366:
367: for (int i = rangeLinks.length - 1; i > -1; i--) {
368: int rangeLink = rangeLinks[i].intValue();
369:
370: if ((rangeLink > 0) && (currentPage - rangeLink >= 1)) {
371: atts.clear();
372: atts.addAttribute("", "type", "type", "CDATA", "prev");
373: atts.addAttribute("", "range", "range", "CDATA",
374: rangeLinks[i].toString());
375: atts.addAttribute("", "uri", "uri", "CDATA", Paginator
376: .encodeURI(requestURI, currentPage, currentPage
377: - rangeLink));
378: atts.addAttribute("", "page", "page", "CDATA", String
379: .valueOf(currentPage - rangeLink));
380: saxTransformer.startElement(Paginator.PAGINATE_URI,
381: "range-link", Paginator.PAGINATE_PREFIX_TOKEN
382: + "range-link", atts);
383: saxTransformer.endElement(Paginator.PAGINATE_URI,
384: "range-link", Paginator.PAGINATE_PREFIX_TOKEN
385: + "range-link");
386: }
387: }
388:
389: for (int i = currentPage - unitLinks; i < currentPage; i++) {
390: if (i > 0) {
391: atts.clear();
392: atts.addAttribute("", "type", "type", "CDATA", "prev");
393: atts.addAttribute("", "uri", "uri", "CDATA", Paginator
394: .encodeURI(requestURI, currentPage, i));
395: atts.addAttribute("", "page", "page", "CDATA", String
396: .valueOf(i));
397: saxTransformer.startElement(Paginator.PAGINATE_URI,
398: "link", Paginator.PAGINATE_PREFIX_TOKEN
399: + "link", atts);
400: saxTransformer.endElement(Paginator.PAGINATE_URI,
401: "link", Paginator.PAGINATE_PREFIX_TOKEN
402: + "link");
403: }
404: }
405: for (int i = currentPage + 1; i <= currentPage + unitLinks; i++) {
406: if (i <= totalPages) {
407: atts.clear();
408: atts.addAttribute("", "type", "type", "CDATA", "next");
409: atts.addAttribute("", "uri", "uri", "CDATA", Paginator
410: .encodeURI(requestURI, currentPage, i));
411: atts.addAttribute("", "page", "page", "CDATA", String
412: .valueOf(i));
413: saxTransformer.startElement(Paginator.PAGINATE_URI,
414: "link", Paginator.PAGINATE_PREFIX_TOKEN
415: + "link", atts);
416: saxTransformer.endElement(Paginator.PAGINATE_URI,
417: "link", Paginator.PAGINATE_PREFIX_TOKEN
418: + "link");
419: }
420: }
421:
422: for (int i = 0; i < rangeLinks.length; i++) {
423: int rangeLink = rangeLinks[i].intValue();
424:
425: if ((rangeLink > 0)
426: && (currentPage + rangeLink <= totalPages)) {
427: atts.clear();
428: atts.addAttribute("", "type", "type", "CDATA", "next");
429: atts.addAttribute("", "range", "range", "CDATA",
430: rangeLinks[i].toString());
431: atts.addAttribute("", "uri", "uri", "CDATA", Paginator
432: .encodeURI(requestURI, currentPage, currentPage
433: + rangeLink));
434: atts.addAttribute("", "page", "page", "CDATA", String
435: .valueOf(currentPage + rangeLink));
436: saxTransformer.startElement(Paginator.PAGINATE_URI,
437: "range-link", Paginator.PAGINATE_PREFIX_TOKEN
438: + "range-link", atts);
439: saxTransformer.endElement(Paginator.PAGINATE_URI,
440: "range-link", Paginator.PAGINATE_PREFIX_TOKEN
441: + "range-link");
442: }
443: }
444:
445: saxTransformer.endElement(Paginator.PAGINATE_URI, "page",
446: Paginator.PAGINATE_PREFIX_TOKEN + "page");
447: }
448:
449: /**
450: * Receive notification of character data.
451: *
452: * @param c The characters from the XML document.
453: * @param start The start position in the array.
454: * @param len The number of characters to read from the array.
455: */
456: public void characters(char c[], int start, int len)
457: throws SAXException {
458: pagesheet.processCharacters(c, start, len);
459: if (pagesheet.isInPage(page, item, itemGroup)) {
460: super .characters(c, start, len);
461: }
462: }
463:
464: /**
465: * Receive notification of ignorable whitespace in element content.
466: *
467: * @param c The characters from the XML document.
468: * @param start The start position in the array.
469: * @param len The number of characters to read from the array.
470: */
471: public void ignorableWhitespace(char c[], int start, int len)
472: throws SAXException {
473: if (pagesheet.isInPage(page, item, itemGroup)) {
474: super .ignorableWhitespace(c, start, len);
475: }
476: }
477:
478: /**
479: * Receive notification of a processing instruction.
480: *
481: * @param target The processing instruction target.
482: * @param data The processing instruction data, or null if none
483: * was supplied.
484: */
485: public void processingInstruction(String target, String data)
486: throws SAXException {
487: if (pagesheet.isInPage(page, item, itemGroup)) {
488: super .processingInstruction(target, data);
489: }
490: }
491:
492: /**
493: * Receive notification of a skipped entity.
494: *
495: * @param name The name of the skipped entity. If it is a
496: * parameter entity, the name will begin with '%'.
497: */
498: public void skippedEntity(String name) throws SAXException {
499: if (pagesheet.isInPage(page, item, itemGroup)) {
500: super .skippedEntity(name);
501: }
502: }
503:
504: /**
505: * Report the start of DTD declarations, if any.
506: *
507: * @param name The document type name.
508: * @param publicId The declared public identifier for the external
509: * DTD subset, or null if none was declared.
510: * @param systemId The declared system identifier for the external
511: * DTD subset, or null if none was declared.
512: */
513: public void startDTD(String name, String publicId, String systemId)
514: throws SAXException {
515: if (pagesheet.isInPage(page, item, itemGroup)) {
516: super .startDTD(name, publicId, systemId);
517: } else {
518: throw new SAXException("Recieved startDTD not in page.");
519: }
520: }
521:
522: /**
523: * Report the end of DTD declarations.
524: */
525: public void endDTD() throws SAXException {
526: if (pagesheet.isInPage(page, item, itemGroup)) {
527: super .endDTD();
528: } else {
529: throw new SAXException("Recieved endDTD not in page.");
530: }
531: }
532:
533: /**
534: * Report the beginning of an entity.
535: *
536: *@param name The name of the entity. If it is a parameter
537: * entity, the name will begin with '%'.
538: */
539: public void startEntity(String name) throws SAXException {
540: if (pagesheet.isInPage(page, item, itemGroup)) {
541: super .startEntity(name);
542: }
543: }
544:
545: /**
546: * Report the end of an entity.
547: *
548: * @param name The name of the entity that is ending.
549: */
550: public void endEntity(String name) throws SAXException {
551: if (pagesheet.isInPage(page, item, itemGroup)) {
552: super .endEntity(name);
553: }
554: }
555:
556: /**
557: * Report the start of a CDATA section.
558: */
559: public void startCDATA() throws SAXException {
560: if (pagesheet.isInPage(page, item, itemGroup)) {
561: super .startCDATA();
562: }
563: }
564:
565: /**
566: * Report the end of a CDATA section.
567: */
568: public void endCDATA() throws SAXException {
569: if (pagesheet.isInPage(page, item, itemGroup)) {
570: super .endCDATA();
571: }
572: }
573:
574: /**
575: * Report an XML comment anywhere in the document.
576: *
577: * @param ch An array holding the characters in the comment.
578: * @param start The starting position in the array.
579: * @param len The number of characters to use from the array.
580: */
581: public void comment(char ch[], int start, int len)
582: throws SAXException {
583: if (pagesheet.isInPage(page, item, itemGroup)) {
584: super .comment(ch, start, len);
585: }
586: }
587:
588: /**
589: * Removes the pagination encoding from the URI by removing the page number
590: * and the previous and next character.
591: */
592: public static String cleanURI(String uri, int current) {
593: String currentS = String.valueOf(current);
594: int index = uri.lastIndexOf(currentS);
595:
596: if (index == -1) {
597: return uri;
598: } else {
599: return uri.substring(0, index - 1)
600: + uri.substring(index + currentS.length() + 1);
601: }
602: }
603:
604: /**
605: * Encode the next page in the given URI. First tries to use the existing
606: * encoding by replacing the current page number, but if the current
607: * encoding is not found it appends "(xx)" to the filename (before the file
608: * extention, if any) where "xx" is the next page value.
609: */
610: public static String encodeURI(String uri, int current, int next) {
611: String currentS = String.valueOf(current);
612: String nextS = String.valueOf(next);
613: int index = uri.lastIndexOf(currentS);
614:
615: if (index == -1) {
616: index = uri.lastIndexOf('.');
617: if (index == -1) {
618: return uri + "(" + nextS + ")";
619: } else {
620: return uri.substring(0, index) + "(" + nextS + ")."
621: + uri.substring(index + 1);
622: }
623: } else {
624: return uri.substring(0, index) + nextS
625: + uri.substring(index + currentS.length());
626: }
627: }
628: }
|