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.Collections;
23  import java.util.List;
24  import javax.servlet.http.HttpServletRequest;
25  import org.apache.velocity.runtime.log.Log;
26  import org.apache.velocity.tools.Scope;
27  import org.apache.velocity.tools.config.DefaultKey;
28  import org.apache.velocity.tools.config.InvalidScope;
29  
30  /**
31   * <p>Abstract view tool for doing "searching" and robust
32   * pagination of search results.  The goal here is to provide a simple
33   * and uniform API for "search tools" that can be used in velocity
34   * templates (or even a standard Search.vm template).  In particular,
35   * this class provides good support for result pagination and some
36   * very simple result caching.
37   * </p>
38   * <p><b>Usage:</b><br>
39   * To use this class, you must extend it and implement
40   * the executeQuery(Object) method.
41   * </p>
42   * <p>
43   * The setCriteria(Object) method takes an Object in order to
44   * allow the search criteria to meet your needs.  Your criteria
45   * may be as simple as a single string, an array of strings, or
46   * whatever you like.  The value passed into this method is that
47   * which will ultimately be passed into executeQuery(Object) to
48   * perform the search and return a list of results.  A simple
49   * implementation might be like:
50   * <pre>
51   * protected List executeQuery(Object crit)
52   * {
53   *     return MyDbUtils.getFooBarsMatching((String)crit);
54   * }
55   * </pre>
56   * <p>
57   * Here's an example of how your subclass would be used in a template:
58   * <pre>
59   *   &lt;form name="search" method="get" action="$link.setRelative('search.vm')"&gt;
60   *     &lt;input type="text"name="find" value="$!search.criteria"&gt;
61   *     &lt;input type="submit" value="Find"&gt;
62   *   &lt;/form&gt;
63   *   #if( $search.hasItems() )
64   *   Showing $!search.pageDescription&lt;br&gt;
65   *     #set( $i = $search.index )
66   *     #foreach( $item in $search.page )
67   *       ${i}. $!item &lt;br&gt;
68   *       #set( $i = $i + 1 )
69   *     #end
70   *     &lt;br&gt;
71   *     #if ( $search.pagesAvailable &gt; 1 )
72   *       #set( $pagelink = $link.setRelative('search.vm').addQueryData("find",$!search.criteria).addQueryData("show",$!search.itemsPerPage) )
73   *       #if( $search.prevIndex )
74   *           &lt;a href="$pagelink.addQueryData('index',$!search.prevIndex)"&gt;Prev&lt;/a&gt;
75   *       #end
76   *       #foreach( $index in $search.slip )
77   *         #if( $index == $search.index )
78   *           &lt;b&gt;$search.pageNumber&lt;/b&gt;
79   *         #else
80   *           &lt;a href="$pagelink.addQueryData('index',$!index)"&gt;$!search.getPageNumber($index)&lt;/a&gt;
81   *         #end
82   *       #end
83   *       #if( $search.nextIndex )
84   *           &lt;a href="$pagelink.addQueryData('index',$!search.nextIndex)"&gt;Next&lt;/a&gt;
85   *       #end
86   *     #end
87   *   #elseif( $search.criteria )
88   *   Sorry, no matches were found for "$!search.criteria".
89   *   #else
90   *   Please enter a search term
91   *   #end
92   * </pre>
93   *
94   * The output of this might look like:<br><br>
95   *   <form method="get" action="">
96   *    <input type="text" value="foo">
97   *    <input type="submit" value="Find">
98   *   </form>
99   *   Showing 1-5 of 8<br>
100  *   1. foo<br>
101  *   2. bar<br>
102  *   3. blah<br>
103  *   4. woogie<br>
104  *   5. baz<br><br>
105  *   <b>1</b> <a href="">2</a> <a href="">Next</a>
106  * </p>
107  * <p>
108  * <b>Example toolbox.xml configuration:</b>
109  * <pre>
110  * &lt;tools&gt;
111  *   &lt;toolbox scope="request"&gt;
112  *     &lt;tool class="com.foo.tools.MySearchTool"/&gt;
113  *   &lt;/toolbox&gt;
114  * &lt;/tools&gt;
115  * </pre>
116  * </p>
117  *
118  * @author Nathan Bubna
119  * @since VelocityTools 2.0
120  * @version $Revision: 591088 $ $Date: 2007-11-01 10:11:41 -0700 (Thu, 01 Nov 2007) $
121  */
122 @DefaultKey("search")
123 @InvalidScope({Scope.APPLICATION,Scope.SESSION})
124 public abstract class AbstractSearchTool extends PagerTool
125 {
126     public static final String DEFAULT_CRITERIA_KEY = "find";
127 
128     /** the key under which StoredResults are kept in session */
129     protected static final String STORED_RESULTS_KEY =
130         StoredResults.class.getName();
131 
132     protected Log LOG;
133     private String criteriaKey = DEFAULT_CRITERIA_KEY;
134     private Object criteria;
135 
136     public void setLog(Log log)
137     {
138         if (log == null)
139         {
140             throw new NullPointerException("log should not be set to null");
141         }
142         this.LOG = log;
143     }
144 
145     /**
146      * Sets the criteria *if* it is set in the request parameters.
147      */
148     public void setup(HttpServletRequest request)
149     {
150         super.setup(request);
151 
152         // only change these settings if they're present in the params
153         String findMe = request.getParameter(getCriteriaKey());
154         if (findMe != null)
155         {
156             setCriteria(findMe);
157         }
158     }
159 
160     /*  ---------------------- mutators -----------------------------  */
161 
162     public void setCriteriaKey(String key)
163     {
164         this.criteriaKey = key;
165     }
166 
167     public String getCriteriaKey()
168     {
169         return this.criteriaKey;
170     }
171 
172 
173     /**
174      * Sets the criteria and results to null, page index to zero, and
175      * items per page to the default.
176      */
177     public void reset()
178     {
179         super.reset();
180         setCriteria(null);
181     }
182 
183 
184     /**
185      * Sets the criteria for this search.
186      *
187      * @param criteria - the criteria used for this search
188      */
189     public void setCriteria(Object criteria)
190     {
191         this.criteria = criteria;
192     }
193 
194 
195     /*  ---------------------- accessors -----------------------------  */
196 
197     /**
198      * Return the criteria object for this request.
199      * (for a simple search mechanism, this will typically be
200      *  just a java.lang.String)
201      *
202      * @return criteria object
203      */
204     public Object getCriteria()
205     {
206         return criteria;
207     }
208 
209 
210     /**
211      * Gets the results for the given criteria either in memory
212      * or by performing a new query for them.  If the criteria
213      * is null, an empty list will be returned.
214      *
215      * @return {@link List} of all items for the criteria
216      */
217     public List getItems()
218     {
219         Object findMe = getCriteria();
220         /* return empty list if we have no criteria */
221         if (findMe == null)
222         {
223             return Collections.EMPTY_LIST;
224         }
225 
226         /* get the current list (should never return null!) */
227         List list = super.getItems();
228         assert (list != null);
229 
230         /* if empty, execute a query for the criteria */
231         if (list.isEmpty())
232         {
233             /* safely perform a new query */
234             try
235             {
236                 list = executeQuery(findMe);
237             }
238             catch (Throwable t)
239             {
240                 if (LOG != null)
241                 {
242                     LOG.error("AbstractSearchTool: executeQuery(" + findMe +
243                               ") failed", t);
244                 }
245             }
246 
247             /* because we can't trust executeQuery() not to return null
248                and getItems() must _never_ return null... */
249             if (list == null)
250             {
251                 list = Collections.EMPTY_LIST;
252             }
253 
254             /* save the new results */
255             setItems(list);
256         }
257         return list;
258     }
259 
260 
261     /*  ---------------------- protected methods -----------------------------  */
262 
263     protected List getStoredItems()
264     {
265         StoredResults sr = getStoredResults();
266 
267         /* if the criteria equals that of the stored results,
268          * then return the stored result list */
269         if (sr != null && getCriteria().equals(sr.getCriteria()))
270         {
271             return sr.getList();
272         }
273         return null;
274     }
275 
276 
277     protected void setStoredItems(List items)
278     {
279         setStoredResults(new StoredResults(getCriteria(), items));
280     }
281 
282 
283     /**
284      * Executes a query for the specified criteria.
285      *
286      * <p>This method must be implemented! A simple
287      * implementation might be something like:
288      * <pre>
289      * protected List executeQuery(Object crit)
290      * {
291      *     return MyDbUtils.getFooBarsMatching((String)crit);
292      * }
293      * </pre>
294      *
295      * @return a {@link List} of results for this query
296      */
297     protected abstract List executeQuery(Object criteria);
298 
299 
300     /**
301      * Retrieves stored search results (if any) from the user's
302      * session attributes.
303      *
304      * @return the {@link StoredResults} retrieved from memory
305      */
306     protected StoredResults getStoredResults()
307     {
308         if (session != null)
309         {
310             return (StoredResults)session.getAttribute(STORED_RESULTS_KEY);
311         }
312         return null;
313     }
314 
315 
316     /**
317      * Stores current search results in the user's session attributes
318      * (if one currently exists) in order to do efficient result pagination.
319      *
320      * <p>Override this to store search results somewhere besides the
321      * HttpSession or to prevent storage of results across requests. In
322      * the former situation, you must also override getStoredResults().</p>
323      *
324      * @param results the {@link StoredResults} to be stored
325      */
326     protected void setStoredResults(StoredResults results)
327     {
328         if (session != null)
329         {
330             session.setAttribute(STORED_RESULTS_KEY, results);
331         }
332     }
333 
334 
335     /*  ---------------------- utility class -----------------------------  */
336 
337     /**
338      * Simple utility class to hold a criterion and its result list.
339      * <p>
340      * This class is by default stored in a user's session,
341      * so it implements Serializable, but its members are
342      * transient. So functionally, it is not serialized and
343      * the last results/criteria will not be persisted if
344      * the session is serialized.
345      * </p>
346      */
347     public static class StoredResults implements java.io.Serializable
348     {
349         /** serial version id */
350         private static final long serialVersionUID = 4503130168585978169L;
351 
352         private final transient Object crit;
353         private final transient List list;
354 
355         /**
356          * Creates a new instance.
357          *
358          * @param crit the criteria for these results
359          * @param list the {@link List} of results to store
360          */
361         public StoredResults(Object crit, List list)
362         {
363             this.crit = crit;
364             this.list = list;
365         }
366 
367         /**
368          * @return the stored criteria object
369          */
370         public Object getCriteria()
371         {
372             return crit;
373         }
374 
375         /**
376          * @return the stored {@link List} of results
377          */
378         public List getList()
379         {
380             return list;
381         }
382 
383     }
384 
385 
386 }