001: package org.apache.velocity.tools.view.tools;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.util.Collections;
023: import java.util.List;
024:
025: /**
026: * <p>Abstract view tool for doing "searching" and robust
027: * pagination of search results. The goal here is to provide a simple
028: * and uniform API for "search tools" that can be used in velocity
029: * templates (or even a standard Search.vm template). In particular,
030: * this class provides good support for result pagination and some
031: * very simple result caching.
032: * </p>
033: * <p><b>Usage:</b><br>
034: * To use this class, you must extend it and implement
035: * the setup(HttpServletRequest) and executeQuery(Object)
036: * methods.
037: * <p>
038: * The setup(HttpServletRequest) method ought to extract
039: * from the current request the search criteria, the current
040: * list index, and optionally, the number of items to display
041: * per page of results. Upon extracting these parameters, they
042: * should be set using the provided setCriteria(Object),
043: * setIndex(int), and setItemsPerPage(int) methods. A simple
044: * implementation would be:
045: * <pre>
046: * public void setup(HttpServletRequest req)
047: * {
048: * ParameterParser pp = new ParameterParser(req);
049: * setCriteria(pp.getString("find"));
050: * setIndex(pp.getInt("index", 0));
051: * setItemsPerPage(pp.getInt("show", DEFAULT_ITEMS_PER_PAGE));
052: * }
053: * </pre>
054: * <p>
055: * The setCriteria(Object) method takes an Object in order to
056: * allow the search criteria to meet your needs. Your criteria
057: * may be as simple as a single string, an array of strings, or
058: * whatever you like. The value passed into this method is that
059: * which will ultimately be passed into executeQuery(Object) to
060: * perform the search and return a list of results. A simple
061: * implementation might be like:
062: * <pre>
063: * protected List executeQuery(Object crit)
064: * {
065: * return MyDbUtils.getFooBarsMatching((String)crit);
066: * }
067: * </pre>
068: * <p>
069: * Here's an example of how your subclass would be used in a template:
070: * <pre>
071: * <form name="search" method="get" action="$link.setRelative('search.vm')">
072: * <input type="text"name="find" value="$!search.criteria">
073: * <input type="submit" value="Find">
074: * </form>
075: * #if( $search.hasItems() )
076: * Showing $!search.pageDescription<br>
077: * #set( $i = $search.index )
078: * #foreach( $item in $search.page )
079: * ${i}. $!item <br>
080: * #set( $i = $i + 1 )
081: * #end
082: * <br>
083: * #if ( $search.pagesAvailable > 1 )
084: * #set( $pagelink = $link.setRelative('search.vm').addQueryData("find",$!search.criteria).addQueryData("show",$!search.itemsPerPage) )
085: * #if( $search.prevIndex )
086: * <a href="$pagelink.addQueryData('index',$!search.prevIndex)">Prev</a>
087: * #end
088: * #foreach( $index in $search.slip )
089: * #if( $index == $search.index )
090: * <b>$search.pageNumber</b>
091: * #else
092: * <a href="$pagelink.addQueryData('index',$!index)">$!search.getPageNumber($index)</a>
093: * #end
094: * #end
095: * #if( $search.nextIndex )
096: * <a href="$pagelink.addQueryData('index',$!search.nextIndex)">Next</a>
097: * #end
098: * #end
099: * #elseif( $search.criteria )
100: * Sorry, no matches were found for "$!search.criteria".
101: * #else
102: * Please enter a search term
103: * #end
104: * </pre>
105: *
106: * The output of this might look like:<br><br>
107: * <form method="get" action="">
108: * <input type="text" value="foo">
109: * <input type="submit" value="Find">
110: * </form>
111: * Showing 1-5 of 8<br>
112: * 1. foo<br>
113: * 2. bar<br>
114: * 3. blah<br>
115: * 4. woogie<br>
116: * 5. baz<br><br>
117: * <b>1</b> <a href="">2</a> <a href="">Next</a>
118: * </p>
119: * <p>
120: * <b>Example toolbox.xml configuration:</b>
121: * <pre><tool>
122: * <key>search</key>
123: * <scope>request</scope>
124: * <class>com.foo.tools.MySearchTool</class>
125: * </tool>
126: * </pre>
127: * </p>
128: *
129: * @author Nathan Bubna
130: * @since VelocityTools 1.0
131: * @version $Revision: 483617 $ $Date: 2006-12-07 11:20:19 -0800 (Thu, 07 Dec 2006) $
132: */
133: public abstract class AbstractSearchTool extends AbstractPagerTool {
134: /** the key under which StoredResults are kept in session */
135: protected static final String STORED_RESULTS_KEY = StoredResults.class
136: .getName();
137:
138: private Object criteria;
139:
140: /* ---------------------- mutators ----------------------------- */
141:
142: /**
143: * Sets the criteria and results to null, page index to zero, and
144: * items per page to the default.
145: */
146: public void reset() {
147: super .reset();
148: criteria = null;
149: }
150:
151: /**
152: * Sets the criteria for this search.
153: *
154: * @param criteria - the criteria used for this search
155: */
156: public void setCriteria(Object criteria) {
157: this .criteria = criteria;
158: }
159:
160: /* ---------------------- accessors ----------------------------- */
161:
162: /**
163: * Return the criteria object for this request.
164: * (for a simple search mechanism, this will typically be
165: * just a java.lang.String)
166: *
167: * @return criteria object
168: */
169: public Object getCriteria() {
170: return criteria;
171: }
172:
173: /**
174: * @deprecated Use {@link AbstractPagerTool#hasItems()}
175: */
176: public boolean hasResults() {
177: return hasItems();
178: }
179:
180: /**
181: * @deprecated Use {@link AbstractPagerTool#getItems()}.
182: */
183: public List getResults() {
184: return getItems();
185: }
186:
187: /**
188: * Gets the results for the given criteria either in memory
189: * or by performing a new query for them. If the criteria
190: * is null, an empty list will be returned.
191: *
192: * @return {@link List} of all items for the criteria
193: */
194: public List getItems() {
195: /* return empty list if we have no criteria */
196: if (criteria == null) {
197: return Collections.EMPTY_LIST;
198: }
199:
200: /* get the current list (should never return null!) */
201: List list = super .getItems();
202:
203: /* if empty, execute a query for the criteria */
204: if (list.isEmpty()) {
205: /* perform a new query */
206: list = executeQuery(criteria);
207:
208: /* because we can't trust executeQuery() not to return null
209: and getItems() must _never_ return null... */
210: if (list == null) {
211: list = Collections.EMPTY_LIST;
212: }
213:
214: /* save the new results */
215: setItems(list);
216: }
217: return list;
218: }
219:
220: /* ---------------------- protected methods ----------------------------- */
221:
222: protected List getStoredItems() {
223: StoredResults sr = getStoredResults();
224:
225: /* if the criteria equals that of the stored results,
226: * then return the stored result list */
227: if (sr != null && criteria.equals(sr.getCriteria())) {
228: return sr.getList();
229: }
230: return null;
231: }
232:
233: protected void setStoredItems(List items) {
234: setStoredResults(new StoredResults(criteria, items));
235: }
236:
237: /**
238: * Executes a query for the specified criteria.
239: *
240: * <p>This method must be implemented! A simple
241: * implementation might be something like:
242: * <pre>
243: * protected List executeQuery(Object crit)
244: * {
245: * return MyDbUtils.getFooBarsMatching((String)crit);
246: * }
247: * </pre>
248: *
249: * @return a {@link List} of results for this query
250: */
251: protected abstract List executeQuery(Object criteria);
252:
253: /**
254: * Retrieves stored search results (if any) from the user's
255: * session attributes.
256: *
257: * @return the {@link StoredResults} retrieved from memory
258: */
259: protected StoredResults getStoredResults() {
260: if (session != null) {
261: return (StoredResults) session
262: .getAttribute(STORED_RESULTS_KEY);
263: }
264: return null;
265: }
266:
267: /**
268: * Stores current search results in the user's session attributes
269: * (if one currently exists) in order to do efficient result pagination.
270: *
271: * <p>Override this to store search results somewhere besides the
272: * HttpSession or to prevent storage of results across requests. In
273: * the former situation, you must also override getStoredResults().</p>
274: *
275: * @param results the {@link StoredResults} to be stored
276: */
277: protected void setStoredResults(StoredResults results) {
278: if (session != null) {
279: session.setAttribute(STORED_RESULTS_KEY, results);
280: }
281: }
282:
283: /* ---------------------- utility class ----------------------------- */
284:
285: /**
286: * Simple utility class to hold a criterion and its result list.
287: * <p>
288: * This class is by default stored in a user's session,
289: * so it implements Serializable, but its members are
290: * transient. So functionally, it is not serialized and
291: * the last results/criteria will not be persisted if
292: * the session is serialized.
293: * </p>
294: */
295: public class StoredResults implements java.io.Serializable {
296:
297: /** serial version id */
298: private static final long serialVersionUID = 4503130168585978169L;
299:
300: private transient Object crit;
301: private transient List list;
302:
303: /**
304: * Creates a new instance.
305: *
306: * @param crit the criteria for these results
307: * @param list the {@link List} of results to store
308: */
309: public StoredResults(Object crit, List list) {
310: this .crit = crit;
311: this .list = list;
312: }
313:
314: /**
315: * @return the stored criteria object
316: */
317: public Object getCriteria() {
318: return crit;
319: }
320:
321: /**
322: * @return the stored {@link List} of results
323: */
324: public List getList() {
325: return list;
326: }
327:
328: }
329:
330: }
|