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:
018: package org.apache.cocoon.transformation.pagination;
019:
020: import java.util.ArrayList;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.Map;
024:
025: import org.apache.cocoon.Modifiable;
026: import org.apache.cocoon.util.ResizableContainer;
027: import org.xml.sax.Attributes;
028: import org.xml.sax.SAXException;
029: import org.xml.sax.helpers.DefaultHandler;
030:
031: /**
032: * Interprets the pagesheet rules to perform pagination.
033: *
034: * <pre>
035: * FIXME (SM): this code sucks! It was done to show the concept of
036: * rule driven pagination (which I find very nice) but
037: * it needs major refactoring in order to be sufficiently
038: * stable to allow any input to enter without breaking
039: * SAX well-formness. I currently don't have the time to make
040: * it any better (along with implementing the char-based rule
041: * that is mostly useful for text documents) but if you want
042: * to blast the code and rewrite it better, you'll make me happy :)
043: * </pre>
044: *
045: * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
046: * @author <a href="mailto:bhtek@yahoo.com">Boon Hian Tek</a>
047: * @version CVS $Id: Pagesheet.java 433543 2006-08-22 06:22:54Z crossley $
048: */
049:
050: /*
051:
052: This is an example pagesheet to show the power of this:
053:
054: <?xml version="1.0"?>
055: <pagesheet xmlns="http://apache.org/cocoon/paginate/1.0">
056: <items>
057: <group name="pictures" element="file" namespace="http://apache.org/cocoon/directory/2.0"/>
058: </items>
059: <rules page="1">
060: <count type="element" name="file" namespace="http://apache.org/cocoon/directory/2.0" num="16"/>
061: <link type="unit" num="2"/>
062: <link type="range" value="10"/>
063: </rules>
064: <rules>
065: <count type="element" name="file" namespace="http://apache.org/cocoon/directory/2.0" num="16"/>
066: <link type="unit" num="5"/>
067: <link type="range" value="20"/>
068: </rules>
069: <rules>
070: <count type="element" name="file" namespace="http://apache.org/cocoon/directory/2.0" num="16"/>
071: <link type="unit" num="5"/>
072: <link type="range" value="2"/>
073: <link type="range" value="5"/>
074: <link type="range" value="10"/>
075: <link type="range" value="20"/>
076: <link type="range" value="100"/>
077: </rules>
078: </pagesheet>
079:
080: which indicates that:
081:
082: 1) there is one item group called "picture" and each item is given by the
083: element "file" of the namespace "http://apache.org/cocoon/directory/2.0".
084:
085: 2) for the first page, the pagination rules indicate that there are two unit
086: links (two above and two below, so linking to page -2 -1 0 +1 +2) and
087: range links have value 10 (so they link to page -10 and +10).
088:
089: 3) for the rest of the pages, there are three unit links (-3 -2 -1 0 +1 +2 +3)
090: and range goes 20 (so +20 and -20).
091:
092: 4) if more than one ranges are defined, range links will be created in sequence
093:
094: 5) range links will be from big to small (eg. 20, 10, then 5) for backward links,
095: range links will be from small to big (eg. 5, 10, then 20) for forward links
096:
097: 6) range link(s) will have an attribute 'range' to indicate the range size
098:
099: */
100: public class Pagesheet extends DefaultHandler implements Cloneable,
101: Modifiable {
102:
103: // Used only during parsing of pagesheet document
104: private int level = 0;
105: private int pg = 0;
106: private long lastModified;
107: private PageRules rules;
108:
109: // Loaded pagesheet information
110: ResizableContainer pageRules;
111:
112: Map itemGroupsPerName;
113: Map itemGroupsPerElement;
114: Map itemListsPerName;
115: Map itemListsPerElement;
116:
117: // Runtime information
118: private ResizableContainer pages;
119: private Page currentPage = null;
120: private int pageCounter = 1;
121: private int elementCounter = 0;
122: private int descendant = 0;
123:
124: private static class Page {
125: public int elementStart;
126: public int elementEnd;
127: public int characters;
128:
129: public Page(PageRules rules, int elementStart) {
130: this .elementStart = elementStart;
131:
132: if (rules.elementCount > 0) {
133: this .elementEnd = this .elementStart
134: + rules.elementCount - 1;
135: } else {
136: this .elementEnd = this .elementStart + 1;
137: }
138: }
139:
140: public boolean validInPage(int elementCounter) {
141: return (this .elementStart <= elementCounter)
142: && (elementCounter <= this .elementEnd);
143: }
144: }
145:
146: private static class ItemList extends ArrayList {
147: public ItemList(int capacity) {
148: super (capacity);
149: }
150:
151: public void addItem(int page) {
152: this .add(new Integer(page));
153: }
154:
155: public int getPageForItem(int item) {
156: Integer i = (Integer) this .get(item - 1);
157:
158: return (i == null) ? 0 : i.intValue();
159: }
160:
161: public boolean valid(int item) {
162: return (item == this .size());
163: }
164: }
165:
166: public Pagesheet() {
167: this .pages = new ResizableContainer(2);
168: }
169:
170: private Pagesheet(ResizableContainer rules, Map itemGroupsPerName,
171: Map itemGroupsPerElement) {
172: this .pageRules = rules;
173: this .itemGroupsPerName = itemGroupsPerName;
174: this .itemGroupsPerElement = itemGroupsPerElement;
175:
176: this .pages = new ResizableContainer(5);
177:
178: if ((this .itemGroupsPerName != null)
179: && (this .itemGroupsPerElement != null)) {
180: this .itemListsPerName = new HashMap(itemGroupsPerName
181: .size());
182: this .itemListsPerElement = new HashMap(itemGroupsPerName
183: .size());
184:
185: Iterator iter = itemGroupsPerName.values().iterator();
186:
187: for (; iter.hasNext();) {
188: ItemGroup group = (ItemGroup) iter.next();
189: ItemList list = new ItemList(10);
190:
191: this .itemListsPerName.put(group.getName(), list);
192: this .itemListsPerElement.put(group.getElementURI()
193: + group.getElementName(), list);
194: }
195: }
196: }
197:
198: // --------------- interprets the pagesheet document ----------------
199:
200: public void startPrefixMapping(String prefix, String uri)
201: throws SAXException {
202: if (!uri.equals(Paginator.PAGINATE_URI)) {
203: throw new SAXException(
204: "The pagesheet's namespace is not supported.");
205: }
206: }
207:
208: public void startElement(String uri, String loc, String raw,
209: Attributes a) throws SAXException {
210: level++;
211: switch (level) {
212: case 1:
213: if (loc.equals("pagesheet")) {
214: // This object represents pagesheet
215: return;
216: }
217: break;
218:
219: case 2:
220: if (loc.equals("rules")) {
221: if (this .pageRules == null) {
222: this .pageRules = new ResizableContainer(2);
223: }
224: String key = a.getValue("page");
225:
226: if (key != null) {
227: try {
228: pg = Integer.parseInt(key);
229: } catch (NumberFormatException e) {
230: throw new SAXException(
231: "Syntax error: the attribute 'rules/@page' must contain a number");
232: }
233: } else {
234: pg = 0;
235: }
236: rules = new PageRules();
237: return;
238: } else if (loc.equals("items")) {
239: if (this .itemGroupsPerName == null) {
240: this .itemGroupsPerName = new HashMap(2);
241: }
242: if (this .itemGroupsPerElement == null) {
243: this .itemGroupsPerElement = new HashMap(2);
244: }
245: return;
246: }
247: break;
248:
249: case 3:
250: if (loc.equals("count")) {
251: rules.elementName = a.getValue("name");
252: rules.elementURI = a.getValue("namespace");
253:
254: if (a.getValue("type").equals("element")) {
255: try {
256: rules.elementCount = Integer.parseInt(a
257: .getValue("num"));
258: } catch (NumberFormatException e) {
259: throw new SAXException(
260: "Syntax error: the attribute 'count/@num' must contain a number");
261: }
262: } else if (a.getValue("type").equals("chars")) {
263: try {
264: rules.charCount = Integer.parseInt(a
265: .getValue("num"));
266: } catch (NumberFormatException e) {
267: throw new SAXException(
268: "Syntax error: the attribute 'count/@num' must contain a number.");
269: }
270: } else {
271: throw new SAXException(
272: "Syntax error: count type not supported.");
273: }
274: return;
275: } else if (loc.equals("link")) {
276: if (a.getValue("type").equals("unit")) {
277: try {
278: rules.unitLinks = Integer.parseInt(a
279: .getValue("num"));
280: } catch (NumberFormatException e) {
281: throw new SAXException(
282: "Syntax error: the attribute 'link/@num' must contain a number.");
283: }
284: } else if (a.getValue("type").equals("range")) {
285: try {
286: rules.addRangeLink(a.getValue("value"));
287: } catch (NumberFormatException e) {
288: throw new SAXException(
289: "Syntax error: the attribute 'link/@value' must contain a number.");
290: }
291: } else {
292: throw new SAXException(
293: "Syntax error: link type not supported.");
294: }
295: return;
296: } else if (loc.equals("group")) {
297: String name = a.getValue("name");
298:
299: if (name == null) {
300: throw new SAXException(
301: "Syntax error: the attribute 'group/@name' must be present.");
302: }
303: String elementName = a.getValue("element");
304:
305: if (elementName == null) {
306: throw new SAXException(
307: "Syntax error: the attribute 'group/@element' must be present.");
308: }
309: String elementURI = a.getValue("namespace");
310: ItemGroup group = new ItemGroup(name, elementURI,
311: elementName);
312:
313: this .itemGroupsPerName.put(name, group);
314: this .itemGroupsPerElement.put(elementURI + elementName,
315: group);
316: return;
317: }
318: }
319: throw new SAXException("Syntax error: element " + raw
320: + " is not recognized or is misplaced.");
321: }
322:
323: public void endElement(String uri, String loc, String raw)
324: throws SAXException {
325: level--;
326: if (loc.equals("rules")) {
327: pageRules.set(pg, rules);
328: }
329: }
330:
331: public void endDocument() throws SAXException {
332: if (pageRules.size() == 0) {
333: throw new SAXException(
334: "Pagesheet must contain at least a set of pagination rules.");
335: }
336: if (pageRules.get(0) == null) {
337: throw new SAXException(
338: "Pagesheet must contain the global pagination rules.");
339: }
340: }
341:
342: // --------------- process the received element events ----------------
343:
344: public void processStartElement(String uri, String name) {
345: PageRules rules = getPageRules(pageCounter);
346:
347: if (rules.match(name, uri)) {
348: elementCounter++;
349: descendant++;
350:
351: if (currentPage == null) {
352: currentPage = new Page(rules, 1);
353: }
354:
355: if (elementCounter > currentPage.elementEnd) {
356: /*System.out.println(">>>> "+pageCounter+
357: ": Starting new page!!! >>> "+
358: elementCounter);*/
359: pageCounter++;
360: currentPage = new Page(rules,
361: currentPage.elementEnd + 1);
362: }
363:
364: pages.set(pageCounter, currentPage);
365: }
366:
367: if (itemGroupsPerElement != null) {
368: String qname = uri + name;
369: ItemGroup group = (ItemGroup) this .itemGroupsPerElement
370: .get(qname);
371:
372: if ((group != null) && (group.match(uri))) {
373: ItemList list = (ItemList) this .itemListsPerElement
374: .get(qname);
375:
376: if (list != null) {
377: list.addItem(pageCounter);
378: }
379: }
380: }
381: }
382:
383: public void processEndElement(String uri, String name) {
384: PageRules rules = getPageRules(pageCounter);
385:
386: if (rules.match(name, uri)) {
387: descendant--;
388:
389: if ((rules.charCount > 0)
390: && (currentPage.characters > rules.charCount)) {
391: // We are over character limit. Flip the page.
392: // System.out.println(">>>> " + pageCounter + ": Flipping page!!!");
393: currentPage.elementEnd = elementCounter;
394: } else if (rules.elementCount == 0) {
395: // No limit on elements is specified, and limit on characters is not reached yet.
396: currentPage.elementEnd++;
397: }
398: }
399: }
400:
401: public void processCharacters(char[] ch, int index, int len) {
402: if (descendant > 0) {
403: // Count amount of characters in the currect page.
404: // System.out.println(">>>> " + pageCounter + ": " + new String(ch, index, len) + " (" + len + " bytes)");
405: currentPage.characters += len;
406: }
407: }
408:
409: // --------------- return the pagination information ----------------
410:
411: public boolean isInPage(int page, int item, String itemGroup) {
412: return ((descendant == 0) || valid(page, item, itemGroup));
413: }
414:
415: public int getTotalPages() {
416: return pageCounter;
417: }
418:
419: public int getTotalItems(String itemGroup) {
420: if (this .itemListsPerName == null) {
421: return 0;
422: }
423: ItemList list = (ItemList) this .itemListsPerName.get(itemGroup);
424:
425: return (list == null) ? 0 : list.size();
426: }
427:
428: public int getPageForItem(int item, String itemGroup) {
429: if (this .itemListsPerName == null) {
430: return 0;
431: }
432: ItemList list = (ItemList) this .itemListsPerName.get(itemGroup);
433:
434: return (list == null) ? 0 : list.getPageForItem(item);
435: }
436:
437: public int itemCount(String elementURI, String elementName) {
438: if (this .itemListsPerElement == null) {
439: return 0;
440: }
441: ItemList list = (ItemList) this .itemListsPerElement
442: .get(elementURI + elementName);
443:
444: return (list == null) ? 0 : list.size();
445: }
446:
447: public String getItemGroupName(String elementURI, String elementName) {
448: if (this .itemListsPerElement == null) {
449: return null;
450: }
451: return ((ItemGroup) this .itemGroupsPerElement.get(elementURI
452: + elementName)).getName();
453: }
454:
455: // ---------------- miscellaneous methods ----------------------------
456:
457: private boolean valid(int page, int item, String itemGroup) {
458: if (item == 0) {
459: Page p = (Page) pages.get(page);
460:
461: return (p != null) && (p.validInPage(elementCounter));
462: } else {
463: if (this .itemListsPerElement == null) {
464: return false;
465: }
466: ItemList list = (ItemList) this .itemListsPerName
467: .get(itemGroup);
468:
469: return (list != null) && (list.valid(item));
470: }
471: }
472:
473: public PageRules getPageRules(int page) {
474: PageRules p = (PageRules) pageRules.get(page);
475:
476: return (p != null) ? p : (PageRules) pageRules.get(0);
477: }
478:
479: public void setLastModified(long lastModified) {
480: this .lastModified = lastModified;
481: }
482:
483: public boolean modifiedSince(long date) {
484: return (this .lastModified == 0 || date != this .lastModified);
485: }
486:
487: public Object clone() {
488: return new Pagesheet(pageRules, itemGroupsPerName,
489: itemGroupsPerElement);
490: }
491: }
|