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: 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 }