KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > velocity > tools > view > tools > AbstractSearchTool


1 /*
2  * Copyright 2003 The Apache Software Foundation.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16
17
18 package org.apache.velocity.tools.view.tools;
19
20
21 import java.util.ArrayList JavaDoc;
22 import java.util.Collections JavaDoc;
23 import java.util.List JavaDoc;
24
25 import javax.servlet.http.HttpServletRequest JavaDoc;
26 import javax.servlet.http.HttpSession JavaDoc;
27
28 import org.apache.velocity.tools.view.context.ViewContext;
29 import org.apache.velocity.tools.view.tools.ViewTool;
30
31
32 /**
33  * <p>Abstract view tool for doing "searching" and robust
34  * pagination of search results. The goal here is to provide a simple
35  * and uniform API for "search tools" that can be used in velocity
36  * templates (or even a standard Search.vm template). In particular,
37  * this class provides good support for result pagination and some
38  * very simple result caching.
39  * </p>
40  * <p><b>Usage:</b><br>
41  * To use this class, you must extend it and implement
42  * the setup(HttpServletRequest) and executeQuery(Object)
43  * methods.
44  * <p>
45  * The setup(HttpServletRequest) method ought to extract
46  * from the current request the search criteria, the current
47  * list index, and optionally, the number of items to display
48  * per page of results. Upon extracting these parameters, they
49  * should be set using the provided setCriteria(Object),
50  * setIndex(int), and setItemsPerPage(int) methods. A simple
51  * implementation would be:
52  * <pre>
53  * public void setup(HttpServletRequest req)
54  * {
55  * ParameterParser pp = new ParameterParser(req);
56  * setCriteria(pp.getString("find"));
57  * setIndex(pp.getInt("index", 0));
58  * setItemsPerPage(pp.getInt("show", DEFAULT_ITEMS_PER_PAGE));
59  * }
60  * </pre>
61  * <p>
62  * The setCriteria(Object) method takes an Object in order to
63  * allow the search criteria to meet your needs. Your criteria
64  * may be as simple as a single string, an array of strings, or
65  * whatever you like. The value passed into this method is that
66  * which will ultimately be passed into executeQuery(Object) to
67  * perform the search and return a list of results. A simple
68  * implementation might be like:
69  * <pre>
70  * protected List executeQuery(Object crit)
71  * {
72  * return MyDbUtils.getFooBarsMatching((String)crit);
73  * }
74  * </pre>
75  * <p>
76  * Here's an example of how your subclass would be used in a template:
77  * <pre>
78  * &lt;form name="search" method="get" action="$link.setRelative('search.vm')"&gt;
79  * &lt;input type="text"name="find" value="$!search.criteria"&gt;
80  * &lt;input type="submit" value="Find"&gt;
81  * &lt;/form&gt;
82  * #if( $search.hasResults() )
83  * Showing $!search.pageDescription&lt;br&gt;
84  * #set( $i = $search.index )
85  * #foreach( $result in $search.page )
86  * ${i}. $!result &lt;br&gt;
87  * #set( $i = $i + 1 )
88  * #end
89  * &lt;br&gt;
90  * #if ( $search.pagesAvailable &gt; 1 )
91  * #set( $searchlink = $link.setRelative('search.vm').addQueryData("find",$!search.criteria).addQueryData("show",$!search.itemsPerPage) )
92  * #if( $search.prevIndex )
93  * &lt;a HREF="$searchlink.addQueryData('index',$!search.prevIndex)"&gt;Prev&lt;/a&gt;
94  * #end
95  * #foreach( $index in $search.slip )
96  * #if( $index == $search.index )
97  * &lt;b&gt;$search.pageNumber&lt;/b&gt;
98  * #else
99  * &lt;a HREF="$searchlink.addQueryData('index',$!index)"&gt;$!search.getPageNumber($index)&lt;/a&gt;
100  * #end
101  * #end
102  * #if( $search.nextIndex )
103  * &lt;a HREF="$searchlink.addQueryData('index',$!search.nextIndex)"&gt;Next&lt;/a&gt;
104  * #end
105  * #end
106  * #elseif( $search.criteria )
107  * Sorry, no matches were found for "$!search.criteria".
108  * #else
109  * Please enter a search term
110  * #end
111  * </pre>
112  *
113  * The output of this might look like:<br><br>
114  * <form method="get" action="">
115  * <input type="text" value="foo">
116  * <input type="submit" value="Find">
117  * </form>
118  * Showing 1-5 of 8<br>
119  * 1. foo<br>
120  * 2. bar<br>
121  * 3. blah<br>
122  * 4. woogie<br>
123  * 5. baz<br><br>
124  * <b>1</b> <a HREF="">2</a> <a HREF="">Next</a>
125  * </p>
126  * <p>
127  * <b>Example toolbox.xml configuration:</b>
128  * <pre>&lt;tool&gt;
129  * &lt;key&gt;search&lt;/key&gt;
130  * &lt;scope&gt;request&lt;/scope&gt;
131  * &lt;class&gt;com.foo.tools.MyAbstractSearchTool&lt;/class&gt;
132  * &lt;/tool&gt;
133  * </pre>
134  * </p>
135  *
136  * @author <a HREF="mailto:nathan@esha.com">Nathan Bubna</a>
137  * @since VelocityTools 1.0
138  * @version $Revision: 1.6.2.1 $ $Date: 2004/03/12 20:16:28 $
139  */

140 public abstract class AbstractSearchTool implements ViewTool
141 {
142
143
144     /** the default number of results shown per page */
145     public static final int DEFAULT_ITEMS_PER_PAGE = 10;
146
147     /** the default max number of result page indices to list */
148     public static final int DEFAULT_SLIP_SIZE = 20;
149
150     /** the key under which StoredResults are kept in session */
151     protected static final String JavaDoc STORED_RESULTS_KEY =
152         StoredResults.class.getName();
153
154     private List JavaDoc results;
155     private Object JavaDoc criteria;
156     private int index = 0;
157     private int slipSize = DEFAULT_SLIP_SIZE;
158     private int itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
159
160     protected HttpSession JavaDoc session;
161
162
163     /**
164      * Initializes this instance by grabbing the request
165      * and session objects from the current ViewContext.
166      *
167      * @param obj the current ViewContext
168      * @throws ClassCastException if the param is not a ViewContext
169      */

170     public void init(Object JavaDoc obj)
171     {
172         ViewContext context = (ViewContext)obj;
173         HttpServletRequest JavaDoc request = context.getRequest();
174         session = request.getSession(false);
175         setup(request);
176     }
177
178
179     /**
180      * Abstract method to make it as obvious as possible just
181      * where implementing classes should be retrieving and configuring
182      * search/display parameters.
183      * <p>A simple implementation would be:
184      * <pre>
185      * public void setup(HttpServletRequest req)
186      * {
187      * ParameterParser pp = new ParameterParser(req);
188      * setCriteria(pp.getString("find"));
189      * setIndex(pp.getInt("index", 0));
190      * setItemsPerPage(pp.getInt("show", DEFAULT_ITEMS_PER_PAGE));
191      * }
192      * </pre>
193      *
194      * @param request the current HttpServletRequest
195      */

196     public abstract void setup(HttpServletRequest JavaDoc request);
197
198
199     /* ---------------------- mutators ----------------------------- */
200
201
202     /**
203      * Sets the criteria and results to null, page index to zero, and
204      * items per page to the default.
205      */

206     public void reset()
207     {
208         results = null;
209         criteria = null;
210         index = 0;
211         itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
212     }
213
214
215     /**
216      * Sets the criteria for this search and clears existing results.
217      *
218      * @param criteria - the criteria used for this search
219      */

220     public void setCriteria(Object JavaDoc criteria)
221     {
222         this.criteria = criteria;
223         this.results = null;
224     }
225
226
227     /**
228      * Sets the index of the first result in the current page
229      *
230      * @param index the result index to start the current page with
231      */

232     public void setIndex(int index)
233     {
234         if (index < 0)
235         {
236             /* quietly override to a reasonable value */
237             index = 0;
238         }
239         this.index = index;
240     }
241
242
243     /**
244      * Sets the number of items returned in a page of results
245      *
246      * @param itemsPerPage the number of items to be returned per page
247      */

248     public void setItemsPerPage(int itemsPerPage)
249     {
250         if (itemsPerPage < 1)
251         {
252             /* quietly override to a reasonable value */
253             itemsPerPage = DEFAULT_ITEMS_PER_PAGE;
254         }
255         this.itemsPerPage = itemsPerPage;
256     }
257
258
259     /**
260      * Sets the number of result page indices for {@link #getSlip} to list.
261      * (for google-ish result page links).
262      *
263      * @see #getSlip
264      * @param slipSize - the number of result page indices to list
265      */

266     public void setSlipSize(int slipSize)
267     {
268         if (slipSize < 2)
269         {
270             /* quietly override to a reasonable value */
271             slipSize = DEFAULT_SLIP_SIZE;
272         }
273         this.slipSize = slipSize;
274     }
275
276
277     /* ---------------------- accessors ----------------------------- */
278
279
280     /**
281      * Return the criteria object for this request.
282      * (for a simple search mechanism, this will typically be
283      * just a java.lang.String)
284      *
285      * @return criteria object
286      */

287     public Object JavaDoc getCriteria()
288     {
289         return criteria;
290     }
291
292
293     /**
294      * Return the set number of items to be displayed per page of results
295      *
296      * @return current number of results shown per page
297      */

298     public int getItemsPerPage()
299     {
300         return itemsPerPage;
301     }
302
303
304     /**
305      * Returns the number of result page indices {@link #getSlip}
306      * will return per request (if available).
307      *
308      * @return the number of result page indices {@link #getSlip}
309      * will try to return
310      */

311     public int getSlipSize()
312     {
313         return slipSize;
314     }
315
316
317     /**
318      * Return the current search result index.
319      *
320      * @return the index for the beginning of the current page
321      */

322     public int getIndex()
323     {
324         return index;
325     }
326
327
328     /**
329      * Checks whether or not the result list is empty.
330      *
331      * @return <code>true</code> if the result list is not empty.
332      */

333     public boolean hasResults()
334     {
335         return !getResults().isEmpty();
336     }
337
338
339     /**
340      * Return the results for the given criteria. This is guaranteed
341      * to never return <code>null</code>.
342      *
343      * @return {@link List} of all results for the criteria
344      */

345     public List JavaDoc getResults()
346     {
347         if (results == null)
348         {
349             results = retrieveResults();
350         }
351         return results;
352     }
353
354
355     /**
356      * Return the index for the next page of results
357      * (as determined by the current index, items per page, and
358      * the number of results). If no "next page" exists, then null is
359      * returned.
360      *
361      * @return index for the next page or <code>null</code> if none exists
362      */

363     public Integer JavaDoc getNextIndex()
364     {
365         int next = index + itemsPerPage;
366         if (next < getResults().size())
367         {
368             return new Integer JavaDoc(next);
369         }
370         return null;
371     }
372
373
374     /**
375      * Return the index for the previous page of results
376      * (as determined by the current index, items per page, and
377      * the number of results). If no "next page" exists, then null is
378      * returned.
379      *
380      * @return index for the previous page or <code>null</code> if none exists
381      */

382     public Integer JavaDoc getPrevIndex()
383     {
384         int prev = Math.min(index, getResults().size()) - itemsPerPage;
385         if (index > 0)
386         {
387             return new Integer JavaDoc(Math.max(0, prev));
388         }
389         return null;
390     }
391
392
393     /**
394      * Return the number of pages that can be made from this list
395      * given the set number of items per page.
396      */

397     public int getPagesAvailable()
398     {
399         return (int)Math.ceil(getResults().size() / (double)itemsPerPage);
400     }
401
402
403     /**
404      * Return the current "page" of search results.
405      *
406      * @return a {@link List} of results for the "current page"
407      */

408     public List JavaDoc getPage()
409     {
410         /* return null if we have no results */
411         if (!hasResults())
412         {
413             return null;
414         }
415         /* quietly keep the page indices to legal values for robustness' sake */
416         int start = Math.min(getResults().size() - 1, index);
417         int end = Math.min(getResults().size(), index + itemsPerPage);
418         return getResults().subList(start, end);
419     }
420
421
422     /**
423      * Returns the "page number" for the specified index. Because the page
424      * number is used for the user interface, the page numbers are 1-based.
425      *
426      * @param i the index that you want the page number for
427      * @return the approximate "page number" for the specified index or
428      * <code>null</code> if there are no results
429      */

430     public Integer JavaDoc getPageNumber(int i)
431     {
432         if (!hasResults())
433         {
434             return null;
435         }
436         return new Integer JavaDoc(1 + i / itemsPerPage);
437     }
438
439
440     /**
441      * Returns the "page number" for the current index. Because the page
442      * number is used for the user interface, the page numbers are 1-based.
443      *
444      * @return the approximate "page number" for the current index or
445      * <code>null</code> if there are no results
446      */

447     public Integer JavaDoc getPageNumber()
448     {
449         return getPageNumber(index);
450     }
451
452
453     /**
454      * <p>Returns a description of the current page. This implementation
455      * displays a 1-based range of result indices and the total number
456      * of results. (e.g. "1 - 10 of 42" or "7 of 7")</p>
457      *
458      * <p>Sub-classes may override this to provide a customized
459      * description (such as one in another language).</p>
460      *
461      * @return a description of the current page
462      */

463     public String JavaDoc getPageDescription()
464     {
465         StringBuffer JavaDoc out = new StringBuffer JavaDoc();
466         int first = index + 1;
467         int total = getResults().size();
468         if (first >= total)
469         {
470             out.append(total);
471             out.append(" of ");
472             out.append(total);
473         }
474         else
475         {
476             int last = Math.min(index + itemsPerPage, total);
477             out.append(first);
478             out.append(" - ");
479             out.append(last);
480             out.append(" of ");
481             out.append(total);
482         }
483         return out.toString();
484     }
485
486
487     /**
488      * Return a <b>S</b>liding <b>L</b>ist of <b>I</b>ndices for <b>P</b>ages
489      * of search results.
490      *
491      * <p>Essentially, this returns a list of result indices that correspond
492      * to available pages of search results (as based on the set
493      * items-per-page). This makes it relativly easy to do a google-ish set
494      * of links to result pages.</p>
495      *
496      * <p>Note that this list of Integers is 0-based to correspond with the
497      * underlying result indices and not the displayed page numbers (see
498      * {@link #getPageNumber}).</p>
499      *
500      * @return {@link List} of Integers representing the indices of result
501      * pages or empty list if there's one or less pages available
502      */

503     public List JavaDoc getSlip()
504     {
505         /* return an empty list if there's no pages to list */
506         int totalPgs = getPagesAvailable();
507         if (totalPgs <= 1)
508         {
509             return Collections.EMPTY_LIST;
510         }
511
512         /* page number is 1-based so decrement it */
513         int curPg = getPageNumber().intValue() - 1;
514
515         /* don't include current page in slip size calcs */
516         int adjSlipSize = slipSize - 1;
517
518         /* start at zero or just under half of max slip size
519          * this keeps "forward" and "back" pages about even
520          * but gives preference to "forward" pages */

521         int slipStart = Math.max(0, (curPg - (adjSlipSize / 2)));
522
523         /* push slip end as far as possible */
524         int slipEnd = Math.min(totalPgs, (slipStart + adjSlipSize));
525
526         /* if we're out of "forward" pages, then push the
527          * slip start toward zero to maintain slip size */

528         if (slipEnd - slipStart < adjSlipSize)
529         {
530             slipStart = Math.max(0, slipEnd - adjSlipSize);
531         }
532
533         /* convert 0-based page numbers to indices and create list */
534         List JavaDoc slip = new ArrayList JavaDoc((slipEnd - slipStart) + 1);
535         for (int i=slipStart; i < slipEnd; i++)
536         {
537             slip.add(new Integer JavaDoc(i * itemsPerPage));
538         }
539         return slip;
540     }
541
542
543     /* ---------------------- private methods ----------------------------- */
544
545
546     /**
547      * Gets the results for the given criteria either in memory
548      * or by performing a new query for them. This is guaranteed to
549      * NOT return null.
550      */

551     private List JavaDoc retrieveResults()
552     {
553         /* return empty list if we have no criteria */
554         if (criteria == null)
555         {
556             return Collections.EMPTY_LIST;
557         }
558
559         /* get any stored results */
560         StoredResults sr = getStoredResults();
561
562         /* if the criteria equals that of the stored results,
563          * then return the stored result list */

564         if (sr != null && criteria.equals(sr.getCriteria()))
565         {
566             return sr.getList();
567         }
568
569         /* perform a new query and make sure we don't end up with a null list */
570         List JavaDoc list = executeQuery(criteria);
571         if (list == null)
572         {
573             list = Collections.EMPTY_LIST;
574         }
575
576         /* save the new results */
577         setStoredResults(new StoredResults(criteria, list));
578         return list;
579     }
580
581
582     /* ---------------------- protected methods ----------------------------- */
583
584
585     /**
586      * Executes a query for the specified criteria.
587      *
588      * <p>This method must be implemented! A simple
589      * implementation might be something like:
590      * <pre>
591      * protected List executeQuery(Object crit)
592      * {
593      * return MyDbUtils.getFooBarsMatching((String)crit);
594      * }
595      * </pre>
596      *
597      * @return a {@link List} of results for this query
598      */

599     protected abstract List JavaDoc executeQuery(Object JavaDoc criteria);
600
601
602     /**
603      * Retrieves stored search results (if any) from the user's
604      * session attributes.
605      *
606      * @return the {@link StoredResults} retrieved from memory
607      */

608     protected StoredResults getStoredResults()
609     {
610         if (session != null)
611         {
612             return (StoredResults)session.getAttribute(STORED_RESULTS_KEY);
613         }
614         return null;
615     }
616
617
618     /**
619      * Stores current search results in the user's session attributes
620      * (if one currently exists) in order to do efficient result pagination.
621      *
622      * <p>Override this to store search results somewhere besides the
623      * HttpSession or to prevent storage of results across requests. In
624      * the former situation, you must also override getStoredResults().</p>
625      *
626      * @param results the {@link StoredResults} to be stored
627      */

628     protected void setStoredResults(StoredResults results)
629     {
630         if (session != null)
631         {
632             session.setAttribute(STORED_RESULTS_KEY, results);
633         }
634     }
635
636
637     /* ---------------------- utility class ----------------------------- */
638
639
640     /**
641      * Simple utility class to hold a criterion and its result list.
642      * <p>
643      * This class is by default stored in a user's session,
644      * so it implements Serializable, but its members are
645      * transient. So functionally, it is not serialized and
646      * the last results/criteria will not be persisted if
647      * the session is serialized.
648      * </p>
649      */

650     public class StoredResults implements java.io.Serializable JavaDoc
651     {
652
653         private transient Object JavaDoc crit;
654         private transient List JavaDoc list;
655
656         /**
657          * Creates a new instance.
658          *
659          * @param crit the criteria for these results
660          * @param list the {@link List} of results to store
661          */

662         public StoredResults(Object JavaDoc crit, List JavaDoc list)
663         {
664             this.crit = crit;
665             this.list = list;
666         }
667
668         /**
669          * @return the stored criteria object
670          */

671         public Object JavaDoc getCriteria()
672         {
673             return crit;
674         }
675
676         /**
677          * @return the stored {@link List} of results
678          */

679         public List JavaDoc getList()
680         {
681             return list;
682         }
683
684     }
685
686
687 }
688
Popular Tags