View Javadoc

1   package org.apache.velocity.tools.view;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import javax.servlet.http.HttpServletRequest;
26  import javax.servlet.http.HttpSession;
27  import org.apache.velocity.tools.Scope;
28  import org.apache.velocity.tools.config.DefaultKey;
29  import org.apache.velocity.tools.config.InvalidScope;
30  
31  /**
32   * <p>View tool for doing request-based pagination of
33   * items in an a list.
34   * </p>
35   * <p><b>Usage:</b><br>
36   * To use this class, you typically push a List of items to it
37   * by putting it in the request attributes under the value returned by
38   * {@link #getNewItemsKey()} (default is "new.items").
39   * You can also set the list of items to be paged in a subclass
40   * using the setItems(List) method, or you can always set the
41   * item list at another point (even from within the template). This
42   * need only happen once per session if a session is available, but the
43   * item list can be (re)set as often as you like.
44   * </p>
45   * <p>
46   * Here's an example of how your subclass would be used in a template:
47   * <pre>
48   *   #if( $pager.hasItems() )
49   *   Showing $!pager.pageDescription&lt;br&gt;
50   *     #set( $i = $pager.index )
51   *     #foreach( $item in $pager.page )
52   *       ${i}. $!item &lt;br&gt;
53   *       #set( $i = $i + 1 )
54   *     #end
55   *     &lt;br&gt;
56   *     #if ( $pager.pagesAvailable &gt; 1 )
57   *       #set( $pagelink = $link.self.param("show",$!pager.itemsPerPage) )
58   *       #if( $pager.prevIndex )
59   *           &lt;a href="$pagelink.param('index',$!pager.prevIndex)"&gt;Prev&lt;/a&gt;
60   *       #end
61   *       #foreach( $index in $pager.slip )
62   *         #if( $index == $pager.index )
63   *           &lt;b&gt;$pager.pageNumber&lt;/b&gt;
64   *         #else
65   *           &lt;a href="$pagelink.param('index',$!index)"&gt;$!pager.getPageNumber($index)&lt;/a&gt;
66   *         #end
67   *       #end
68   *       #if( $pager.nextIndex )
69   *           &lt;a href="$pagelink.param('index',$!pager.nextIndex)"&gt;Next&lt;/a&gt;
70   *       #end
71   *     #end
72   *   #else
73   *   No items in list.
74   *   #end
75   * </pre>
76   *
77   * The output of this might look like:<br><br>
78   *   Showing 1-5 of 8<br>
79   *   1. foo<br>
80   *   2. bar<br>
81   *   3. blah<br>
82   *   4. woogie<br>
83   *   5. baz<br><br>
84   *   <b>1</b> <a href="">2</a> <a href="">Next</a>
85   * </p>
86   * <p>
87   * <b>Example tools.xml configuration:</b>
88   * <pre>
89   * &lt;tools&gt;
90   *   &lt;toolbox scope="request"&gt;
91   *     &lt;tool class="org.apache.velocity.tools.view.PagerTool"/&gt;
92   *   &lt;/toolbox&gt;
93   * &lt;/tools&gt;
94   * </pre>
95   * </p>
96   *
97   * @author Nathan Bubna
98   * @version $Revision: 595822 $ $Date: 2007-11-16 13:07:51 -0800 (Fri, 16 Nov 2007) $
99   * @since VelocityTools 2.0
100  */
101 @DefaultKey("pager")
102 @InvalidScope({Scope.APPLICATION,Scope.SESSION})
103 public class PagerTool
104 {
105     public static final String DEFAULT_NEW_ITEMS_KEY = "new.items";
106     public static final String DEFAULT_INDEX_KEY = "index";
107     public static final String DEFAULT_ITEMS_PER_PAGE_KEY = "show";
108     public static final String DEFAULT_SLIP_SIZE_KEY = "slipSize";
109 
110     /** the default number of items shown per page */
111     public static final int DEFAULT_ITEMS_PER_PAGE = 10;
112 
113     /** the default max number of page indices to list */
114     public static final int DEFAULT_SLIP_SIZE = 20;
115 
116     /** the key under which items are stored in session */
117     protected static final String STORED_ITEMS_KEY = PagerTool.class.getName();
118 
119     private String newItemsKey = DEFAULT_NEW_ITEMS_KEY;
120     private String indexKey = DEFAULT_INDEX_KEY;
121     private String itemsPerPageKey = DEFAULT_ITEMS_PER_PAGE_KEY;
122     private String slipSizeKey = DEFAULT_SLIP_SIZE_KEY;
123     private boolean createSession = false;
124 
125     private List items;
126     private int index = 0;
127     private int slipSize = DEFAULT_SLIP_SIZE;
128     private int itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
129     protected HttpSession session;
130 
131     /**
132      * Initializes this tool with the specified {@link HttpServletRequest}.
133      * This is required for this tool to operate and will throw a
134      * NullPointerException if this is not set or is set to {@code null}.
135      */
136     public void setRequest(HttpServletRequest request)
137     {
138         if (request == null)
139         {
140             throw new NullPointerException("request should not be null");
141         }
142         this.session = request.getSession(getCreateSession());
143         setup(request);
144     }
145 
146     /**
147      * Sets the index, itemsPerPage, and/or slipSize *if* they are set
148      * in the request parameters.  Likewise, this will set the item list
149      * to be paged *if* there is a list pushed into the request attributes
150      * under the {@link #getNewItemsKey()}.
151      *
152      * @param request the current HttpServletRequest
153      */
154     public void setup(HttpServletRequest request)
155     {
156         ParameterTool params = new ParameterTool(request);
157 
158         // only change these settings if they're present in the params
159         int index = params.getInt(getIndexKey(), -1);
160         if (index >= 0)
161         {
162             setIndex(index);
163         }
164         int show = params.getInt(getItemsPerPageKey(), 0);
165         if (show > 0)
166         {
167             setItemsPerPage(show);
168         }
169         int slipSize = params.getInt(getSlipSizeKey(), 0);
170         if (slipSize > 0)
171         {
172             setSlipSize(slipSize);
173         }
174 
175         // look for items in the request attributes
176         List newItems = (List)request.getAttribute(getNewItemsKey());
177         if (newItems != null)
178         {
179             // only set the items if a list was pushed into the request
180             setItems(newItems);
181         }
182     }
183 
184 
185     public void setNewItemsKey(String key)
186     {
187         this.newItemsKey = key;
188     }
189 
190     public String getNewItemsKey()
191     {
192         return this.newItemsKey;
193     }
194 
195     public void setIndexKey(String key)
196     {
197         this.indexKey = key;
198     }
199 
200     public String getIndexKey()
201     {
202         return this.indexKey;
203     }
204 
205     public void setItemsPerPageKey(String key)
206     {
207         this.itemsPerPageKey = key;
208     }
209 
210     public String getItemsPerPageKey()
211     {
212         return this.itemsPerPageKey;
213     }
214 
215     public void setSlipSizeKey(String key)
216     {
217         this.slipSizeKey = key;
218     }
219 
220     public String getSlipSizeKey()
221     {
222         return this.slipSizeKey;
223     }
224 
225     public void setCreateSession(boolean createSession)
226     {
227         this.createSession = createSession;
228     }
229 
230     public boolean getCreateSession()
231     {
232         return this.createSession;
233     }
234 
235     /**
236      * Sets the item list to null, page index to zero, and
237      * items per page to the default.
238      */
239     public void reset()
240     {
241         items = null;
242         index = 0;
243         itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
244     }
245 
246     /**
247      * Sets the List to page through.
248      *
249      * @param items - the  {@link List} of items to be paged through
250      */
251     public void setItems(List items)
252     {
253         this.items = items;
254         setStoredItems(items);
255     }
256 
257     /**
258      * Sets the index of the first result in the current page
259      *
260      * @param index the result index to start the current page with
261      */
262     public void setIndex(int index)
263     {
264         if (index < 0)
265         {
266             /* quietly override to a reasonable value */
267             index = 0;
268         }
269         this.index = index;
270     }
271 
272     /**
273      * Sets the number of items returned in a page of items
274      *
275      * @param itemsPerPage the number of items to be returned per page
276      */
277     public void setItemsPerPage(int itemsPerPage)
278     {
279         if (itemsPerPage < 1)
280         {
281             /* quietly override to a reasonable value */
282             itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
283         }
284         this.itemsPerPage = itemsPerPage;
285     }
286 
287     /**
288      * Sets the number of result page indices for {@link #getSlip} to list.
289      * (for google-ish result page links).
290      *
291      * @see #getSlip
292      * @param slipSize - the number of result page indices to list
293      */
294     public void setSlipSize(int slipSize)
295     {
296         if (slipSize < 2)
297         {
298             /* quietly override to a reasonable value */
299             slipSize = DEFAULT_SLIP_SIZE;
300         }
301         this.slipSize = slipSize;
302     }
303 
304     /*  ---------------------- accessors ----------------------------- */
305 
306     /**
307      * Returns the set number of items to be displayed per page of items
308      *
309      * @return current number of items shown per page
310      */
311     public int getItemsPerPage()
312     {
313         return itemsPerPage;
314     }
315 
316     /**
317      * Returns the number of result page indices {@link #getSlip}
318      * will return per request (if available).
319      *
320      * @return the number of result page indices {@link #getSlip}
321      *         will try to return
322      */
323     public int getSlipSize()
324     {
325         return slipSize;
326     }
327 
328 
329     /**
330      * Returns the current search result index.
331      *
332      * @return the index for the beginning of the current page
333      */
334     public int getIndex()
335     {
336         return index;
337     }
338 
339 
340     /**
341      * Checks whether or not the result list is empty.
342      *
343      * @return <code>true</code> if the result list is not empty.
344      */
345     public boolean hasItems()
346     {
347         return !getItems().isEmpty();
348     }
349 
350     /**
351      * Returns the item list. This is guaranteed
352      * to never return <code>null</code>.
353      *
354      * @return {@link List} of all the items
355      */
356     public List getItems()
357     {
358         if (items == null)
359         {
360             items = getStoredItems();
361         }
362 
363         return (items != null) ? items : Collections.EMPTY_LIST;
364     }
365 
366     /**
367      * Returns the index of the last item on the current page of results
368      * (as determined by the current index, items per page, and
369      * the number of items).  If there is no current page, then null is
370      * returned.
371      *
372      * @return index for the last item on this page or <code>null</code>
373      *         if none exists
374      * @since VelocityTools 1.3
375      */
376     public Integer getLastIndex()
377     {
378         if (!hasItems())
379         {
380             return null;
381         }
382         return Integer.valueOf(Math.min(getTotal() - 1, index + itemsPerPage - 1));
383     }
384 
385     /**
386      * Returns the index for the next page of items
387      * (as determined by the current index, items per page, and
388      * the number of items).  If no "next page" exists, then null is
389      * returned.
390      *
391      * @return index for the next page or <code>null</code> if none exists
392      */
393     public Integer getNextIndex()
394     {
395         int next = index + itemsPerPage;
396         if (next < getTotal())
397         {
398             return Integer.valueOf(next);
399         }
400         return null;
401     }
402 
403     /**
404      * Returns the index of the first item on the current page of results
405      * (as determined by the current index, items per page, and
406      * the number of items).  If there is no current page, then null is
407      * returned. This is different than {@link #getIndex()} in that it
408      * is adjusted to fit the reality of the items available and is not a
409      * mere accessor for the current, user-set index value.
410      *
411      * @return index for the first item on this page or <code>null</code>
412      *         if none exists
413      * @since VelocityTools 1.3
414      */
415     public Integer getFirstIndex()
416     {
417         if (!hasItems())
418         {
419             return null;
420         }
421         return Integer.valueOf(Math.min(getTotal() - 1, index));
422     }
423 
424     /**
425      * Return the index for the previous page of items
426      * (as determined by the current index, items per page, and
427      * the number of items).  If no "next page" exists, then null is
428      * returned.
429      *
430      * @return index for the previous page or <code>null</code> if none exists
431      */
432     public Integer getPrevIndex()
433     {
434         int prev = Math.min(index, getTotal()) - itemsPerPage;
435         if (index > 0)
436         {
437             return Integer.valueOf(Math.max(0, prev));
438         }
439         return null;
440     }
441 
442     /**
443      * Returns the number of pages that can be made from this list
444      * given the set number of items per page.
445      */
446     public int getPagesAvailable()
447     {
448         return (int)Math.ceil(getTotal() / (double)itemsPerPage);
449     }
450 
451 
452     /**
453      * Returns the current "page" of search items.
454      *
455      * @return a {@link List} of items for the "current page"
456      */
457     public List getPage()
458     {
459         /* return null if we have no items */
460         if (!hasItems())
461         {
462             return null;
463         }
464         /* quietly keep the page indices to legal values for robustness' sake */
465         int start = getFirstIndex().intValue();
466         int end = getLastIndex().intValue() + 1;
467         return getItems().subList(start, end);
468     }
469 
470     /**
471      * Returns the "page number" for the specified index.  Because the page
472      * number is used for the user interface, the page numbers are 1-based.
473      *
474      * @param i the index that you want the page number for
475      * @return the approximate "page number" for the specified index or
476      *         <code>null</code> if there are no items
477      */
478     public Integer getPageNumber(int i)
479     {
480         if (!hasItems())
481         {
482             return null;
483         }
484         return Integer.valueOf(1 + i / itemsPerPage);
485     }
486 
487 
488     /**
489      * Returns the "page number" for the current index.  Because the page
490      * number is used for the user interface, the page numbers are 1-based.
491      *
492      * @return the approximate "page number" for the current index or
493      *         <code>null</code> if there are no items
494      */
495     public Integer getPageNumber()
496     {
497         return getPageNumber(index);
498     }
499 
500     /**
501      * Returns the total number of items available.
502      * @since VelocityTools 1.3
503      */
504     public int getTotal()
505     {
506         if (!hasItems())
507         {
508             return 0;
509         }
510         return getItems().size();
511     }
512 
513     /**
514      * <p>Returns a description of the current page.  This implementation
515      * displays a 1-based range of result indices and the total number
516      * of items.  (e.g. "1 - 10 of 42" or "7 of 7")  If there are no items,
517      * this will return "0 of 0".</p>
518      *
519      * <p>Sub-classes may override this to provide a customized
520      * description (such as one in another language).</p>
521      *
522      * @return a description of the current page
523      */
524     public String getPageDescription()
525     {
526         if (!hasItems())
527         {
528             return "0 of 0";
529         }
530 
531         StringBuilder out = new StringBuilder();
532         int first = getFirstIndex().intValue() + 1;
533         int total = getTotal();
534         if (first >= total)
535         {
536             out.append(total);
537             out.append(" of ");
538             out.append(total);
539         }
540         else
541         {
542             int last = getLastIndex().intValue() + 1;
543             out.append(first);
544             out.append(" - ");
545             out.append(last);
546             out.append(" of ");
547             out.append(total);
548         }
549         return out.toString();
550     }
551 
552     /**
553      * Returns a <b>S</b>liding <b>L</b>ist of <b>I</b>ndices for <b>P</b>ages
554      * of items.
555      *
556      * <p>Essentially, this returns a list of item indices that correspond
557      * to available pages of items (as based on the set items-per-page).
558      * This makes it relativly easy to do a google-ish set of links to
559      * available pages.</p>
560      *
561      * <p>Note that this list of Integers is 0-based to correspond with the
562      * underlying result indices and not the displayed page numbers (see
563      * {@link #getPageNumber}).</p>
564      *
565      * @return {@link List} of Integers representing the indices of result
566      *         pages or empty list if there's one or less pages available
567      */
568     public List getSlip()
569     {
570         /* return an empty list if there's no pages to list */
571         int totalPgs = getPagesAvailable();
572         if (totalPgs <= 1)
573         {
574             return Collections.EMPTY_LIST;
575         }
576 
577         /* page number is 1-based so decrement it */
578         int curPg = getPageNumber().intValue() - 1;
579 
580         /* start at zero or just under half of max slip size
581          * this keeps "forward" and "back" pages about even
582          * but gives preference to "forward" pages */
583         int slipStart = Math.max(0, (curPg - (slipSize / 2)));
584 
585         /* push slip end as far as possible */
586         int slipEnd = Math.min(totalPgs, (slipStart + slipSize));
587 
588         /* if we're out of "forward" pages, then push the
589          * slip start toward zero to maintain slip size */
590         if (slipEnd - slipStart < slipSize)
591         {
592             slipStart = Math.max(0, slipEnd - slipSize);
593         }
594 
595         /* convert 0-based page numbers to indices and create list */
596         List slip = new ArrayList(slipEnd - slipStart);
597         for (int i=slipStart; i < slipEnd; i++)
598         {
599             slip.add(Integer.valueOf(i * itemsPerPage));
600         }
601         return slip;
602     }
603 
604     /*  ---------------------- protected methods ------------------------  */
605 
606     /**
607      * Retrieves stored search items (if any) from the user's
608      * session attributes.
609      *
610      * @return the {@link List} retrieved from memory
611      */
612     protected List getStoredItems()
613     {
614         if (session != null)
615         {
616             return (List)session.getAttribute(STORED_ITEMS_KEY);
617         }
618         return null;
619     }
620 
621 
622     /**
623      * Stores current search items in the user's session attributes
624      * (if one currently exists) in order to do efficient result pagination.
625      *
626      * <p>Override this to store search items somewhere besides the
627      * HttpSession or to prevent storage of items across requests. In
628      * the former situation, you must also override getStoredItems().</p>
629      *
630      * @param items the {@link List} to be stored
631      */
632     protected void setStoredItems(List items)
633     {
634         if (session != null)
635         {
636             session.setAttribute(STORED_ITEMS_KEY, items);
637         }
638     }
639 
640 }