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.ArrayList; 23 import java.util.Collections; 24 import java.util.List; 25 import javax.servlet.http.HttpServletRequest; 26 import javax.servlet.http.HttpSession; 27 import org.apache.velocity.tools.Scope; 28 import org.apache.velocity.tools.config.DefaultKey; 29 import org.apache.velocity.tools.config.InvalidScope; 30 31 /** 32 * <p>View tool for doing request-based pagination of 33 * items in an a list. 34 * </p> 35 * <p><b>Usage:</b><br> 36 * To use this class, you typically push a List of items to it 37 * by putting it in the request attributes under the value returned by 38 * {@link #getNewItemsKey()} (default is "new.items"). 39 * You can also set the list of items to be paged in a subclass 40 * using the setItems(List) method, or you can always set the 41 * item list at another point (even from within the template). This 42 * need only happen once per session if a session is available, but the 43 * item list can be (re)set as often as you like. 44 * </p> 45 * <p> 46 * Here's an example of how your subclass would be used in a template: 47 * <pre> 48 * #if( $pager.hasItems() ) 49 * Showing $!pager.pageDescription<br> 50 * #set( $i = $pager.index ) 51 * #foreach( $item in $pager.page ) 52 * ${i}. $!item <br> 53 * #set( $i = $i + 1 ) 54 * #end 55 * <br> 56 * #if ( $pager.pagesAvailable > 1 ) 57 * #set( $pagelink = $link.self.param("show",$!pager.itemsPerPage) ) 58 * #if( $pager.prevIndex ) 59 * <a href="$pagelink.param('index',$!pager.prevIndex)">Prev</a> 60 * #end 61 * #foreach( $index in $pager.slip ) 62 * #if( $index == $pager.index ) 63 * <b>$pager.pageNumber</b> 64 * #else 65 * <a href="$pagelink.param('index',$!index)">$!pager.getPageNumber($index)</a> 66 * #end 67 * #end 68 * #if( $pager.nextIndex ) 69 * <a href="$pagelink.param('index',$!pager.nextIndex)">Next</a> 70 * #end 71 * #end 72 * #else 73 * No items in list. 74 * #end 75 * </pre> 76 * 77 * The output of this might look like:<br><br> 78 * Showing 1-5 of 8<br> 79 * 1. foo<br> 80 * 2. bar<br> 81 * 3. blah<br> 82 * 4. woogie<br> 83 * 5. baz<br><br> 84 * <b>1</b> <a href="">2</a> <a href="">Next</a> 85 * </p> 86 * <p> 87 * <b>Example tools.xml configuration:</b> 88 * <pre> 89 * <tools> 90 * <toolbox scope="request"> 91 * <tool class="org.apache.velocity.tools.view.PagerTool"/> 92 * </toolbox> 93 * </tools> 94 * </pre> 95 * </p> 96 * 97 * @author Nathan Bubna 98 * @version $Revision: 595822 $ $Date: 2007-11-16 13:07:51 -0800 (Fri, 16 Nov 2007) $ 99 * @since VelocityTools 2.0 100 */ 101 @DefaultKey("pager") 102 @InvalidScope({Scope.APPLICATION,Scope.SESSION}) 103 public class PagerTool 104 { 105 public static final String DEFAULT_NEW_ITEMS_KEY = "new.items"; 106 public static final String DEFAULT_INDEX_KEY = "index"; 107 public static final String DEFAULT_ITEMS_PER_PAGE_KEY = "show"; 108 public static final String DEFAULT_SLIP_SIZE_KEY = "slipSize"; 109 110 /** the default number of items shown per page */ 111 public static final int DEFAULT_ITEMS_PER_PAGE = 10; 112 113 /** the default max number of page indices to list */ 114 public static final int DEFAULT_SLIP_SIZE = 20; 115 116 /** the key under which items are stored in session */ 117 protected static final String STORED_ITEMS_KEY = PagerTool.class.getName(); 118 119 private String newItemsKey = DEFAULT_NEW_ITEMS_KEY; 120 private String indexKey = DEFAULT_INDEX_KEY; 121 private String itemsPerPageKey = DEFAULT_ITEMS_PER_PAGE_KEY; 122 private String slipSizeKey = DEFAULT_SLIP_SIZE_KEY; 123 private boolean createSession = false; 124 125 private List items; 126 private int index = 0; 127 private int slipSize = DEFAULT_SLIP_SIZE; 128 private int itemsPerPage = DEFAULT_ITEMS_PER_PAGE; 129 protected HttpSession session; 130 131 /** 132 * Initializes this tool with the specified {@link HttpServletRequest}. 133 * This is required for this tool to operate and will throw a 134 * NullPointerException if this is not set or is set to {@code null}. 135 */ 136 public void setRequest(HttpServletRequest request) 137 { 138 if (request == null) 139 { 140 throw new NullPointerException("request should not be null"); 141 } 142 this.session = request.getSession(getCreateSession()); 143 setup(request); 144 } 145 146 /** 147 * Sets the index, itemsPerPage, and/or slipSize *if* they are set 148 * in the request parameters. Likewise, this will set the item list 149 * to be paged *if* there is a list pushed into the request attributes 150 * under the {@link #getNewItemsKey()}. 151 * 152 * @param request the current HttpServletRequest 153 */ 154 public void setup(HttpServletRequest request) 155 { 156 ParameterTool params = new ParameterTool(request); 157 158 // only change these settings if they're present in the params 159 int index = params.getInt(getIndexKey(), -1); 160 if (index >= 0) 161 { 162 setIndex(index); 163 } 164 int show = params.getInt(getItemsPerPageKey(), 0); 165 if (show > 0) 166 { 167 setItemsPerPage(show); 168 } 169 int slipSize = params.getInt(getSlipSizeKey(), 0); 170 if (slipSize > 0) 171 { 172 setSlipSize(slipSize); 173 } 174 175 // look for items in the request attributes 176 List newItems = (List)request.getAttribute(getNewItemsKey()); 177 if (newItems != null) 178 { 179 // only set the items if a list was pushed into the request 180 setItems(newItems); 181 } 182 } 183 184 185 public void setNewItemsKey(String key) 186 { 187 this.newItemsKey = key; 188 } 189 190 public String getNewItemsKey() 191 { 192 return this.newItemsKey; 193 } 194 195 public void setIndexKey(String key) 196 { 197 this.indexKey = key; 198 } 199 200 public String getIndexKey() 201 { 202 return this.indexKey; 203 } 204 205 public void setItemsPerPageKey(String key) 206 { 207 this.itemsPerPageKey = key; 208 } 209 210 public String getItemsPerPageKey() 211 { 212 return this.itemsPerPageKey; 213 } 214 215 public void setSlipSizeKey(String key) 216 { 217 this.slipSizeKey = key; 218 } 219 220 public String getSlipSizeKey() 221 { 222 return this.slipSizeKey; 223 } 224 225 public void setCreateSession(boolean createSession) 226 { 227 this.createSession = createSession; 228 } 229 230 public boolean getCreateSession() 231 { 232 return this.createSession; 233 } 234 235 /** 236 * Sets the item list to null, page index to zero, and 237 * items per page to the default. 238 */ 239 public void reset() 240 { 241 items = null; 242 index = 0; 243 itemsPerPage = DEFAULT_ITEMS_PER_PAGE; 244 } 245 246 /** 247 * Sets the List to page through. 248 * 249 * @param items - the {@link List} of items to be paged through 250 */ 251 public void setItems(List items) 252 { 253 this.items = items; 254 setStoredItems(items); 255 } 256 257 /** 258 * Sets the index of the first result in the current page 259 * 260 * @param index the result index to start the current page with 261 */ 262 public void setIndex(int index) 263 { 264 if (index < 0) 265 { 266 /* quietly override to a reasonable value */ 267 index = 0; 268 } 269 this.index = index; 270 } 271 272 /** 273 * Sets the number of items returned in a page of items 274 * 275 * @param itemsPerPage the number of items to be returned per page 276 */ 277 public void setItemsPerPage(int itemsPerPage) 278 { 279 if (itemsPerPage < 1) 280 { 281 /* quietly override to a reasonable value */ 282 itemsPerPage = DEFAULT_ITEMS_PER_PAGE; 283 } 284 this.itemsPerPage = itemsPerPage; 285 } 286 287 /** 288 * Sets the number of result page indices for {@link #getSlip} to list. 289 * (for google-ish result page links). 290 * 291 * @see #getSlip 292 * @param slipSize - the number of result page indices to list 293 */ 294 public void setSlipSize(int slipSize) 295 { 296 if (slipSize < 2) 297 { 298 /* quietly override to a reasonable value */ 299 slipSize = DEFAULT_SLIP_SIZE; 300 } 301 this.slipSize = slipSize; 302 } 303 304 /* ---------------------- accessors ----------------------------- */ 305 306 /** 307 * Returns the set number of items to be displayed per page of items 308 * 309 * @return current number of items shown per page 310 */ 311 public int getItemsPerPage() 312 { 313 return itemsPerPage; 314 } 315 316 /** 317 * Returns the number of result page indices {@link #getSlip} 318 * will return per request (if available). 319 * 320 * @return the number of result page indices {@link #getSlip} 321 * will try to return 322 */ 323 public int getSlipSize() 324 { 325 return slipSize; 326 } 327 328 329 /** 330 * Returns the current search result index. 331 * 332 * @return the index for the beginning of the current page 333 */ 334 public int getIndex() 335 { 336 return index; 337 } 338 339 340 /** 341 * Checks whether or not the result list is empty. 342 * 343 * @return <code>true</code> if the result list is not empty. 344 */ 345 public boolean hasItems() 346 { 347 return !getItems().isEmpty(); 348 } 349 350 /** 351 * Returns the item list. This is guaranteed 352 * to never return <code>null</code>. 353 * 354 * @return {@link List} of all the items 355 */ 356 public List getItems() 357 { 358 if (items == null) 359 { 360 items = getStoredItems(); 361 } 362 363 return (items != null) ? items : Collections.EMPTY_LIST; 364 } 365 366 /** 367 * Returns the index of the last item on the current page of results 368 * (as determined by the current index, items per page, and 369 * the number of items). If there is no current page, then null is 370 * returned. 371 * 372 * @return index for the last item on this page or <code>null</code> 373 * if none exists 374 * @since VelocityTools 1.3 375 */ 376 public Integer getLastIndex() 377 { 378 if (!hasItems()) 379 { 380 return null; 381 } 382 return Integer.valueOf(Math.min(getTotal() - 1, index + itemsPerPage - 1)); 383 } 384 385 /** 386 * Returns the index for the next page of items 387 * (as determined by the current index, items per page, and 388 * the number of items). If no "next page" exists, then null is 389 * returned. 390 * 391 * @return index for the next page or <code>null</code> if none exists 392 */ 393 public Integer getNextIndex() 394 { 395 int next = index + itemsPerPage; 396 if (next < getTotal()) 397 { 398 return Integer.valueOf(next); 399 } 400 return null; 401 } 402 403 /** 404 * Returns the index of the first item on the current page of results 405 * (as determined by the current index, items per page, and 406 * the number of items). If there is no current page, then null is 407 * returned. This is different than {@link #getIndex()} in that it 408 * is adjusted to fit the reality of the items available and is not a 409 * mere accessor for the current, user-set index value. 410 * 411 * @return index for the first item on this page or <code>null</code> 412 * if none exists 413 * @since VelocityTools 1.3 414 */ 415 public Integer getFirstIndex() 416 { 417 if (!hasItems()) 418 { 419 return null; 420 } 421 return Integer.valueOf(Math.min(getTotal() - 1, index)); 422 } 423 424 /** 425 * Return the index for the previous page of items 426 * (as determined by the current index, items per page, and 427 * the number of items). If no "next page" exists, then null is 428 * returned. 429 * 430 * @return index for the previous page or <code>null</code> if none exists 431 */ 432 public Integer getPrevIndex() 433 { 434 int prev = Math.min(index, getTotal()) - itemsPerPage; 435 if (index > 0) 436 { 437 return Integer.valueOf(Math.max(0, prev)); 438 } 439 return null; 440 } 441 442 /** 443 * Returns the number of pages that can be made from this list 444 * given the set number of items per page. 445 */ 446 public int getPagesAvailable() 447 { 448 return (int)Math.ceil(getTotal() / (double)itemsPerPage); 449 } 450 451 452 /** 453 * Returns the current "page" of search items. 454 * 455 * @return a {@link List} of items for the "current page" 456 */ 457 public List getPage() 458 { 459 /* return null if we have no items */ 460 if (!hasItems()) 461 { 462 return null; 463 } 464 /* quietly keep the page indices to legal values for robustness' sake */ 465 int start = getFirstIndex().intValue(); 466 int end = getLastIndex().intValue() + 1; 467 return getItems().subList(start, end); 468 } 469 470 /** 471 * Returns the "page number" for the specified index. Because the page 472 * number is used for the user interface, the page numbers are 1-based. 473 * 474 * @param i the index that you want the page number for 475 * @return the approximate "page number" for the specified index or 476 * <code>null</code> if there are no items 477 */ 478 public Integer getPageNumber(int i) 479 { 480 if (!hasItems()) 481 { 482 return null; 483 } 484 return Integer.valueOf(1 + i / itemsPerPage); 485 } 486 487 488 /** 489 * Returns the "page number" for the current index. Because the page 490 * number is used for the user interface, the page numbers are 1-based. 491 * 492 * @return the approximate "page number" for the current index or 493 * <code>null</code> if there are no items 494 */ 495 public Integer getPageNumber() 496 { 497 return getPageNumber(index); 498 } 499 500 /** 501 * Returns the total number of items available. 502 * @since VelocityTools 1.3 503 */ 504 public int getTotal() 505 { 506 if (!hasItems()) 507 { 508 return 0; 509 } 510 return getItems().size(); 511 } 512 513 /** 514 * <p>Returns a description of the current page. This implementation 515 * displays a 1-based range of result indices and the total number 516 * of items. (e.g. "1 - 10 of 42" or "7 of 7") If there are no items, 517 * this will return "0 of 0".</p> 518 * 519 * <p>Sub-classes may override this to provide a customized 520 * description (such as one in another language).</p> 521 * 522 * @return a description of the current page 523 */ 524 public String getPageDescription() 525 { 526 if (!hasItems()) 527 { 528 return "0 of 0"; 529 } 530 531 StringBuilder out = new StringBuilder(); 532 int first = getFirstIndex().intValue() + 1; 533 int total = getTotal(); 534 if (first >= total) 535 { 536 out.append(total); 537 out.append(" of "); 538 out.append(total); 539 } 540 else 541 { 542 int last = getLastIndex().intValue() + 1; 543 out.append(first); 544 out.append(" - "); 545 out.append(last); 546 out.append(" of "); 547 out.append(total); 548 } 549 return out.toString(); 550 } 551 552 /** 553 * Returns a <b>S</b>liding <b>L</b>ist of <b>I</b>ndices for <b>P</b>ages 554 * of items. 555 * 556 * <p>Essentially, this returns a list of item indices that correspond 557 * to available pages of items (as based on the set items-per-page). 558 * This makes it relativly easy to do a google-ish set of links to 559 * available pages.</p> 560 * 561 * <p>Note that this list of Integers is 0-based to correspond with the 562 * underlying result indices and not the displayed page numbers (see 563 * {@link #getPageNumber}).</p> 564 * 565 * @return {@link List} of Integers representing the indices of result 566 * pages or empty list if there's one or less pages available 567 */ 568 public List getSlip() 569 { 570 /* return an empty list if there's no pages to list */ 571 int totalPgs = getPagesAvailable(); 572 if (totalPgs <= 1) 573 { 574 return Collections.EMPTY_LIST; 575 } 576 577 /* page number is 1-based so decrement it */ 578 int curPg = getPageNumber().intValue() - 1; 579 580 /* start at zero or just under half of max slip size 581 * this keeps "forward" and "back" pages about even 582 * but gives preference to "forward" pages */ 583 int slipStart = Math.max(0, (curPg - (slipSize / 2))); 584 585 /* push slip end as far as possible */ 586 int slipEnd = Math.min(totalPgs, (slipStart + slipSize)); 587 588 /* if we're out of "forward" pages, then push the 589 * slip start toward zero to maintain slip size */ 590 if (slipEnd - slipStart < slipSize) 591 { 592 slipStart = Math.max(0, slipEnd - slipSize); 593 } 594 595 /* convert 0-based page numbers to indices and create list */ 596 List slip = new ArrayList(slipEnd - slipStart); 597 for (int i=slipStart; i < slipEnd; i++) 598 { 599 slip.add(Integer.valueOf(i * itemsPerPage)); 600 } 601 return slip; 602 } 603 604 /* ---------------------- protected methods ------------------------ */ 605 606 /** 607 * Retrieves stored search items (if any) from the user's 608 * session attributes. 609 * 610 * @return the {@link List} retrieved from memory 611 */ 612 protected List getStoredItems() 613 { 614 if (session != null) 615 { 616 return (List)session.getAttribute(STORED_ITEMS_KEY); 617 } 618 return null; 619 } 620 621 622 /** 623 * Stores current search items in the user's session attributes 624 * (if one currently exists) in order to do efficient result pagination. 625 * 626 * <p>Override this to store search items somewhere besides the 627 * HttpSession or to prevent storage of items across requests. In 628 * the former situation, you must also override getStoredItems().</p> 629 * 630 * @param items the {@link List} to be stored 631 */ 632 protected void setStoredItems(List items) 633 { 634 if (session != null) 635 { 636 session.setAttribute(STORED_ITEMS_KEY, items); 637 } 638 } 639 640 }