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 * <form name="search" method="get" action="$link.setRelative('search.vm')">
60 * <input type="text"name="find" value="$!search.criteria">
61 * <input type="submit" value="Find">
62 * </form>
63 * #if( $search.hasItems() )
64 * Showing $!search.pageDescription<br>
65 * #set( $i = $search.index )
66 * #foreach( $item in $search.page )
67 * ${i}. $!item <br>
68 * #set( $i = $i + 1 )
69 * #end
70 * <br>
71 * #if ( $search.pagesAvailable > 1 )
72 * #set( $pagelink = $link.setRelative('search.vm').addQueryData("find",$!search.criteria).addQueryData("show",$!search.itemsPerPage) )
73 * #if( $search.prevIndex )
74 * <a href="$pagelink.addQueryData('index',$!search.prevIndex)">Prev</a>
75 * #end
76 * #foreach( $index in $search.slip )
77 * #if( $index == $search.index )
78 * <b>$search.pageNumber</b>
79 * #else
80 * <a href="$pagelink.addQueryData('index',$!index)">$!search.getPageNumber($index)</a>
81 * #end
82 * #end
83 * #if( $search.nextIndex )
84 * <a href="$pagelink.addQueryData('index',$!search.nextIndex)">Next</a>
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 * <tools>
111 * <toolbox scope="request">
112 * <tool class="com.foo.tools.MySearchTool"/>
113 * </toolbox>
114 * </tools>
115 * </pre>
116 * </p>
117 *
118 * @author Nathan Bubna
119 * @since VelocityTools 2.0
120 * @version $Revision: 961796 $ $Date: 2010-07-08 17:18:15 +0200 (jeu., 08 juil. 2010) $
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 }