1 package org.apache.torque.util; 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.io.IOException; 23 import java.io.ObjectInputStream; 24 import java.io.Serializable; 25 import java.sql.Connection; 26 import java.util.ArrayList; 27 import java.util.Hashtable; 28 import java.util.Iterator; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Set; 32 33 import org.apache.commons.logging.Log; 34 import org.apache.commons.logging.LogFactory; 35 import org.apache.torque.Torque; 36 import org.apache.torque.TorqueException; 37 import org.apache.torque.criteria.CriteriaInterface; 38 import org.apache.torque.sql.SqlBuilder; 39 40 /** 41 * This class can be used to retrieve a large result set from a database query. 42 * The query is started and then rows are returned a page at a time. The <code> 43 * LargeSelect</code> is meant to be placed into the Session or User.Temp, so 44 * that it can be used in response to several related requests. Note that in 45 * order to use <code>LargeSelect</code> you need to be willing to accept the 46 * fact that the result set may become inconsistent with the database if updates 47 * are processed subsequent to the queries being executed. Specifying a memory 48 * page limit of 1 will give you a consistent view of the records but the totals 49 * may not be accurate and the performance will be terrible. In most cases 50 * the potential for inconsistencies data should not cause any serious problems 51 * and performance should be pretty good (but read on for further warnings). 52 * 53 * <p>The idea here is that the full query result would consume too much memory 54 * and if displayed to a user the page would be too long to be useful. Rather 55 * than loading the full result set into memory, a window of data (the memory 56 * limit) is loaded and retrieved a page at a time. If a request occurs for 57 * data that falls outside the currently loaded window of data then a new query 58 * is executed to fetch the required data. Performance is optimized by 59 * starting a thread to execute the database query and fetch the results. This 60 * will perform best when paging forwards through the data, but a minor 61 * optimization where the window is moved backwards by two rather than one page 62 * is included for when a user pages past the beginning of the window. 63 * 64 * <p>As the query is performed in in steps, it is often the case that the total 65 * number of records and pages of data is unknown. <code>LargeSelect</code> 66 * provides various methods for indicating how many records and pages it is 67 * currently aware of and for presenting this information to users. 68 * 69 * <p><code>LargeSelect</code> utilizes the <code>Criteria</code> methods 70 * <code>setOffset()</code> and <code>setLimit()</code> to limit the amount of 71 * data retrieved from the database - these values are either passed through to 72 * the DBMS when supported (efficient with the caveat below) or handled by 73 * the Torque API when it is not (not so efficient). 74 * 75 * <p>As <code>LargeSelect</code> must re-execute the query each time the user 76 * pages out of the window of loaded data, you should consider the impact of 77 * non-index sort orderings and other criteria that will require the DBMS to 78 * execute the entire query before filtering down to the offset and limit either 79 * internally or via Torque. 80 * 81 * <p>The memory limit defaults to 5 times the page size you specify, but 82 * alternative constructors and the class method <code>setMemoryPageLimit() 83 * </code> allow you to override this for a specific instance of 84 * <code>LargeSelect</code> or future instances respectively. 85 * 86 * <p>Typically you will create a <code>LargeSelect</code> using your <code> 87 * Criteria</code> (perhaps created from the results of a search parameter 88 * page), page size, memory page limit, return class name (for which you may 89 * have defined a business object class before hand) and peer class 90 * and place this in user.Temp thus: 91 * 92 * <pre> 93 * data.getUser().setTemp("someName", largeSelect); 94 * </pre> 95 * 96 * <p>In your template you will then use something along the lines of: 97 * 98 * <pre> 99 * #set($largeSelect = $data.User.getTemp("someName")) 100 * #set($searchop = $data.Parameters.getString("searchop")) 101 * #if($searchop.equals("prev")) 102 * #set($recs = $largeSelect.PreviousResults) 103 * #else 104 * #if($searchop.equals("goto")) 105 * #set($recs = $largeSelect.getPage($data.Parameters.getInt("page", 1))) 106 * #else 107 * #set($recs = $largeSelect.NextResults) 108 * #end 109 * #end 110 * </pre> 111 * 112 * <p>...to move through the records. <code>LargeSelect</code> implements a 113 * number of convenience methods that make it easy to add all of the necessary 114 * bells and whistles to your template. 115 * 116 * @param <T> the type of the objects which are returned on a query. 117 * 118 * @author <a href="mailto:john.mcnally@clearink.com">John D. McNally</a> 119 * @author <a href="mailto:seade@backstagetech.com.au">Scott Eade</a> 120 * @version $Id: LargeSelect.java 1377560 2012-08-27 03:25:41Z tfischer $ 121 */ 122 public class LargeSelect<T> implements Runnable, Serializable 123 { 124 /** Serial version */ 125 private static final long serialVersionUID = -1166842932571491942L; 126 127 /** The number of records that a page consists of. */ 128 private int pageSize; 129 /** The maximum number of records to maintain in memory. */ 130 private int memoryLimit; 131 132 /** The record number of the first record in memory. */ 133 private transient volatile int blockBegin = 0; 134 /** The record number of the last record in memory. */ 135 private transient volatile int blockEnd; 136 /** How much of the memory block is currently occupied with result data. */ 137 private volatile int currentlyFilledTo = -1; 138 139 /** The memory store of results. */ 140 private transient List<T> results = null; 141 142 /** The thread that executes the query. */ 143 private transient Thread thread = null; 144 /** 145 * A flag used to kill the thread when the currently executing query is no 146 * longer required. 147 */ 148 private transient volatile boolean killThread = false; 149 /** A flag that indicates whether or not the query thread is running. */ 150 private transient volatile boolean threadRunning = false; 151 /** 152 * An indication of whether or not the current query has completed 153 * processing. 154 */ 155 private transient volatile boolean queryCompleted = false; 156 /** 157 * An indication of whether or not the totals (records and pages) are at 158 * their final values. 159 */ 160 private transient boolean totalsFinalized = false; 161 162 /** The cursor position in the result set. */ 163 private int position; 164 /** The total number of pages known to exist. */ 165 private int totalPages = -1; 166 /** The total number of records known to exist. */ 167 private int totalRecords = 0; 168 169 /** The criteria used for the query. */ 170 private CriteriaInterface<?> criteria = null; 171 172 /** The last page of results that were returned. */ 173 private transient List<T> lastResults; 174 175 /** 176 * The BasePeerImpl object that handles database selects. 177 */ 178 private BasePeerImpl<T> peer = null; 179 180 /** 181 * The default value (">") used to indicate that the total number of 182 * records or pages is unknown. 183 */ 184 public static final String DEFAULT_MORE_INDICATOR = ">"; 185 186 /** 187 * The value used to indicate that the total number of records or pages is 188 * unknown (default: ">"). You can use <code>setMoreIndicator()</code> 189 * to change this to whatever value you like (e.g. "more than"). 190 */ 191 private String moreIndicator = DEFAULT_MORE_INDICATOR; 192 193 /** 194 * The default value for the maximum number of pages of data to be retained 195 * in memory. 196 */ 197 public static final int DEFAULT_MEMORY_LIMIT_PAGES = 5; 198 199 /** 200 * The maximum number of pages of data to be retained in memory. Use 201 * <code>setMemoryPageLimit()</code> to provide your own value. 202 */ 203 private int memoryPageLimit = DEFAULT_MEMORY_LIMIT_PAGES; 204 205 /** 206 * The number of milliseconds to sleep when the result of a query 207 * is not yet available. 208 */ 209 private static final int QUERY_NOT_COMPLETED_SLEEP_TIME = 500; 210 211 /** 212 * The number of milliseconds to sleep before retrying to stop a query. 213 */ 214 private static final int QUERY_STOP_SLEEP_TIME = 100; 215 216 /** A place to store search parameters that relate to this query. */ 217 private Map<String, String> params = null; 218 219 /** Logging */ 220 private static Log log = LogFactory.getLog(LargeSelect.class); 221 222 /** 223 * Creates a LargeSelect whose results are returned as a <code>List</code> 224 * containing a maximum of <code>pageSize</code> objects of the type 225 * defined within the class named <code>returnBuilderClassName</code> at a 226 * time, maintaining a maximum of <code>LargeSelect.memoryPageLimit</code> 227 * pages of results in memory. 228 * 229 * @param criteria object used by BasePeer to build the query. In order to 230 * allow this class to utilize database server implemented offsets and 231 * limits (when available), the provided criteria must not have any limit or 232 * offset defined. 233 * 234 * @param pageSize number of rows to return in one block. 235 * @param peerImpl the peer that will be used to do the select operations 236 * 237 * @throws IllegalArgumentException if <code>criteria</code> uses one or 238 * both of offset and limit, if <code>pageSize</code> is less than 1 or 239 * the Criteria object does not contain SELECT columns. 240 */ 241 public LargeSelect( 242 CriteriaInterface<?> criteria, 243 int pageSize, 244 BasePeerImpl<T> peerImpl) 245 { 246 this( 247 criteria, 248 pageSize, 249 LargeSelect.DEFAULT_MEMORY_LIMIT_PAGES, 250 peerImpl); 251 } 252 253 /** 254 * Creates a LargeSelect whose results are returned as a <code>List</code> 255 * containing a maximum of <code>pageSize</code> objects of the type T at a 256 * time, maintaining a maximum of <code>memoryPageLimit</code> pages of 257 * results in memory. 258 * 259 * @param criteria object used by BasePeerImpl to build the query. In order to 260 * allow this class to utilize database server implemented offsets and 261 * limits (when available), the provided criteria must not have any limit or 262 * offset defined. 263 * 264 * @param pageSize number of rows to return in one block. 265 * @param memoryPageLimit maximum number of pages worth of rows to be held 266 * in memory at one time. 267 * @param peerImpl the peer that will be used to do the select operations 268 * 269 * @throws IllegalArgumentException if <code>criteria</code> uses one or 270 * both of offset and limit, if <code>pageSize</code> or <code> 271 * memoryLimitPages</code> are less than 1 or the Criteria object does not 272 * contain SELECT columns.. 273 */ 274 public LargeSelect( 275 CriteriaInterface<?> criteria, 276 int pageSize, 277 int memoryPageLimit, 278 BasePeerImpl<T> peerImpl) 279 { 280 this.peer = peerImpl; 281 282 if (criteria.getOffset() != 0 || criteria.getLimit() != -1) 283 { 284 throw new IllegalArgumentException( 285 "criteria must not use Offset and/or Limit."); 286 } 287 288 if (pageSize < 1) 289 { 290 throw new IllegalArgumentException( 291 "pageSize must be greater than zero."); 292 } 293 294 if (memoryPageLimit < 1) 295 { 296 throw new IllegalArgumentException( 297 "memoryPageLimit must be greater than zero."); 298 } 299 300 this.pageSize = pageSize; 301 this.memoryLimit = pageSize * memoryPageLimit; 302 this.criteria = criteria; 303 blockEnd = blockBegin + memoryLimit - 1; 304 startQuery(pageSize); 305 } 306 307 /** 308 * Retrieve a specific page, if it exists. 309 * 310 * @param pageNumber the number of the page to be retrieved - must be 311 * greater than zero. An empty <code>List</code> will be returned if 312 * <code>pageNumber</code> exceeds the total number of pages that exist. 313 * @return a <code>List</code> of query results containing a maximum of 314 * <code>pageSize</code> results. 315 * @throws IllegalArgumentException when <code>pageNo</code> is not 316 * greater than zero. 317 * @throws TorqueException if a sleep is unexpectedly interrupted. 318 */ 319 public List<T> getPage(int pageNumber) throws TorqueException 320 { 321 if (pageNumber < 1) 322 { 323 throw new IllegalArgumentException( 324 "pageNumber must be greater than zero."); 325 } 326 return getResults((pageNumber - 1) * pageSize); 327 } 328 329 /** 330 * Gets the next page of rows. 331 * 332 * @return a <code>List</code> of query results containing a maximum of 333 * <code>pageSize</code> results. 334 * @throws TorqueException if a sleep is unexpectedly interrupted. 335 */ 336 public List<T> getNextResults() throws TorqueException 337 { 338 if (!getNextResultsAvailable()) 339 { 340 return getCurrentPageResults(); 341 } 342 return getResults(position); 343 } 344 345 /** 346 * Provide access to the results from the current page. 347 * 348 * @return a <code>List</code> of query results containing a maximum of 349 * <code>pageSize</code> results. 350 * @throws TorqueException if a sleep is unexpectedly interrupted. 351 */ 352 public List<T> getCurrentPageResults() throws TorqueException 353 { 354 return null == lastResults && position > 0 355 ? getResults(position) : lastResults; 356 } 357 358 /** 359 * Gets the previous page of rows. 360 * 361 * @return a <code>List</code> of query results containing a maximum of 362 * <code>pageSize</code> results. 363 * @throws TorqueException if a sleep is unexpectedly interrupted. 364 */ 365 public List<T> getPreviousResults() throws TorqueException 366 { 367 if (!getPreviousResultsAvailable()) 368 { 369 return getCurrentPageResults(); 370 } 371 372 int start; 373 if (position - 2 * pageSize < 0) 374 { 375 start = 0; 376 } 377 else 378 { 379 start = position - 2 * pageSize; 380 } 381 return getResults(start); 382 } 383 384 /** 385 * Gets a page of rows starting at a specified row. 386 * 387 * @param start the starting row. 388 * @return a <code>List</code> of query results containing a maximum of 389 * <code>pageSize</code> results. 390 * @throws TorqueException if a sleep is unexpectedly interrupted. 391 */ 392 private List<T> getResults(int start) throws TorqueException 393 { 394 return getResults(start, pageSize); 395 } 396 397 /** 398 * Gets a block of rows starting at a specified row and containing a 399 * specified number of rows. 400 * 401 * @param start the starting row. 402 * @param size the number of rows. 403 * @return a <code>List</code> of query results containing a maximum of 404 * <code>pageSize</code> results. 405 * @throws IllegalArgumentException if <code>size > memoryLimit</code> or 406 * <code>start</code> and <code>size</code> result in a situation that is 407 * not catered for. 408 * @throws TorqueException if a sleep is unexpectedly interrupted. 409 */ 410 private synchronized List<T> getResults(int start, int size) 411 throws TorqueException 412 { 413 if (log.isDebugEnabled()) 414 { 415 log.debug("getResults(start: " + start 416 + ", size: " + size + ") invoked."); 417 } 418 419 if (size > memoryLimit) 420 { 421 throw new IllegalArgumentException("size (" + size 422 + ") exceeds memory limit (" + memoryLimit + ")."); 423 } 424 425 // Request was for a block of rows which should be in progress. 426 // If the rows have not yet been returned, wait for them to be 427 // retrieved. 428 if (start >= blockBegin && (start + size - 1) <= blockEnd) 429 { 430 if (log.isDebugEnabled()) 431 { 432 log.debug("getResults(): Sleeping until " 433 + "start+size-1 (" + (start + size - 1) 434 + ") > currentlyFilledTo (" + currentlyFilledTo 435 + ") && !queryCompleted (!" + queryCompleted + ")"); 436 } 437 while (((start + size - 1) > currentlyFilledTo) && !queryCompleted) 438 { 439 try 440 { 441 Thread.sleep(QUERY_NOT_COMPLETED_SLEEP_TIME); 442 } 443 catch (InterruptedException e) 444 { 445 throw new TorqueException("Unexpected interruption", e); 446 } 447 } 448 } 449 450 // Going in reverse direction, trying to limit db hits so assume user 451 // might want at least 2 sets of data. 452 else if (start < blockBegin && start >= 0) 453 { 454 if (log.isDebugEnabled()) 455 { 456 log.debug("getResults(): Paging backwards as start (" + start 457 + ") < blockBegin (" + blockBegin + ") && start >= 0"); 458 } 459 stopQuery(); 460 if (memoryLimit >= 2 * size) 461 { 462 blockBegin = start - size; 463 if (blockBegin < 0) 464 { 465 blockBegin = 0; 466 } 467 } 468 else 469 { 470 blockBegin = start; 471 } 472 blockEnd = blockBegin + memoryLimit - 1; 473 startQuery(size); 474 // Re-invoke getResults() to provide the wait processing. 475 return getResults(start, size); 476 } 477 478 // Assume we are moving on, do not retrieve any records prior to start. 479 else if ((start + size - 1) > blockEnd) 480 { 481 if (log.isDebugEnabled()) 482 { 483 log.debug("getResults(): Paging past end of loaded data as " 484 + "start+size-1 (" + (start + size - 1) 485 + ") > blockEnd (" + blockEnd + ")"); 486 } 487 stopQuery(); 488 blockBegin = start; 489 blockEnd = blockBegin + memoryLimit - 1; 490 startQuery(size); 491 // Re-invoke getResults() to provide the wait processing. 492 return getResults(start, size); 493 } 494 495 else 496 { 497 throw new IllegalArgumentException("Parameter configuration not " 498 + "accounted for."); 499 } 500 501 int fromIndex = start - blockBegin; 502 int toIndex = fromIndex + Math.min(size, results.size() - fromIndex); 503 504 if (log.isDebugEnabled()) 505 { 506 log.debug("getResults(): Retrieving records from results elements " 507 + "start-blockBegin (" + fromIndex + ") through " 508 + "fromIndex + Math.min(size, results.size() - fromIndex) (" 509 + toIndex + ")"); 510 } 511 512 List<T> returnResults; 513 514 synchronized (results) 515 { 516 returnResults = new ArrayList<T>( 517 results.subList(fromIndex, toIndex)); 518 } 519 520 position = start + size; 521 lastResults = returnResults; 522 return returnResults; 523 } 524 525 /** 526 * A background thread that retrieves the rows. 527 */ 528 public void run() 529 { 530 /* The connection to the database. */ 531 Connection conn = null; 532 533 try 534 { 535 // Add 1 to memory limit to check if the query ends on a page break. 536 results = new ArrayList<T>(memoryLimit + 1); 537 538 criteria.setOffset(blockBegin); 539 // Add 1 to memory limit to check if the query ends on a 540 // page break. 541 criteria.setLimit(memoryLimit + 1); 542 543 /* 544 * Fix criterions relating to booleanint or booleanchar columns 545 * The defaultTableMap parameter in this call is null because we have 546 * no default peer class inside LargeSelect. This means that all 547 * columns not fully qualified will not be modified. 548 */ 549 String query; 550 if (criteria instanceof Criteria) 551 { 552 peer.correctBooleans((Criteria) criteria); 553 peer.setDbName((Criteria) criteria); 554 query = SqlBuilder.buildQuery((Criteria) criteria).toString(); 555 } 556 else 557 { 558 peer.correctBooleans( 559 (org.apache.torque.criteria.Criteria) criteria); 560 peer.setDbName((org.apache.torque.criteria.Criteria) criteria); 561 query = SqlBuilder.buildQuery( 562 (org.apache.torque.criteria.Criteria) criteria) 563 .toString(); 564 } 565 566 // Execute the query. 567 if (log.isDebugEnabled()) 568 { 569 log.debug("run(): query = " + query); 570 log.debug("run(): memoryLimit = " + memoryLimit); 571 log.debug("run(): blockBegin = " + blockBegin); 572 log.debug("run(): blockEnd = " + blockEnd); 573 } 574 575 // Get a connection to the db. 576 conn = Transaction.begin(criteria.getDbName()); 577 578 // Continue getting rows one page at a time until the memory limit 579 // is reached, all results have been retrieved, or the rest 580 // of the results have been determined to be irrelevant. 581 boolean allRecordsRetrieved = false; 582 while (!killThread 583 && !allRecordsRetrieved 584 && currentlyFilledTo + pageSize <= blockEnd) 585 { 586 if (log.isDebugEnabled()) 587 { 588 log.debug("run(): Invoking BasePeerImpl.doSelect()"); 589 } 590 591 List<T> tempResults; 592 if (criteria instanceof Criteria) 593 { 594 tempResults = peer.doSelect( 595 (Criteria) criteria, 596 conn); 597 } 598 else 599 { 600 tempResults = peer.doSelect( 601 (org.apache.torque.criteria.Criteria) criteria, 602 conn); 603 } 604 605 if (tempResults.size() < criteria.getLimit()) 606 { 607 allRecordsRetrieved = true; 608 } 609 610 synchronized (results) 611 { 612 results.addAll(tempResults); 613 } 614 615 currentlyFilledTo += tempResults.size(); 616 617 boolean perhapsLastPage = true; 618 619 // If the extra record was indeed found then we know we are not 620 // on the last page but we must now get rid of it. 621 if (results.size() == memoryLimit + 1) 622 { 623 synchronized (results) 624 { 625 results.remove(currentlyFilledTo--); 626 } 627 perhapsLastPage = false; 628 } 629 630 if (results.size() > 0 631 && blockBegin + currentlyFilledTo >= totalRecords) 632 { 633 // Add 1 because index starts at 0 634 totalRecords = blockBegin + currentlyFilledTo + 1; 635 } 636 637 // if the db has limited the datasets, we must retrieve all 638 // datasets. 639 if (allRecordsRetrieved) 640 { 641 queryCompleted = true; 642 // The following ugly condition ensures that the totals are 643 // not finalized when a user does something like requesting 644 // a page greater than what exists in the database. 645 if (perhapsLastPage 646 && getCurrentPageNumber() <= getTotalPages()) 647 { 648 totalsFinalized = true; 649 } 650 } 651 } 652 653 Transaction.commit(conn); 654 conn = null; 655 656 if (log.isDebugEnabled()) 657 { 658 log.debug("run(): While loop terminated because either:"); 659 log.debug("run(): 1. qds.allRecordsRetrieved(): " 660 + allRecordsRetrieved); 661 log.debug("run(): 2. killThread: " + killThread); 662 log.debug("run(): 3. !(currentlyFilledTo + size <= blockEnd): !" 663 + (currentlyFilledTo + pageSize <= blockEnd)); 664 log.debug("run(): - currentlyFilledTo: " + currentlyFilledTo); 665 log.debug("run(): - size: " + pageSize); 666 log.debug("run(): - blockEnd: " + blockEnd); 667 log.debug("run(): - results.size(): " + results.size()); 668 } 669 } 670 catch (Exception e) 671 { 672 log.error(e); 673 } 674 finally 675 { 676 if (conn != null) 677 { 678 Transaction.safeRollback(conn); 679 } 680 threadRunning = false; 681 682 // Make sure getResults() finally returns if we die. 683 queryCompleted = true; 684 685 if (log.isDebugEnabled()) 686 { 687 log.debug("Exiting query thread"); 688 } 689 } 690 } 691 692 /** 693 * Starts a new thread to retrieve the result set. 694 * 695 * @param initialSize the initial size for each block. 696 */ 697 private synchronized void startQuery(int initialSize) 698 { 699 if (log.isDebugEnabled()) 700 { 701 log.debug("Starting query thread"); 702 } 703 if (!threadRunning) 704 { 705 pageSize = initialSize; 706 currentlyFilledTo = -1; 707 queryCompleted = false; 708 thread = new Thread(this); 709 thread.setName("LargeSelect query Thread"); 710 thread.start(); 711 threadRunning = true; 712 if (log.isDebugEnabled()) 713 { 714 log.debug("query thread started"); 715 } 716 } 717 } 718 719 /** 720 * Used to stop filling the memory with the current block of results, if it 721 * has been determined that they are no longer relevant. 722 * 723 * @throws TorqueException if a sleep is interrupted. 724 */ 725 private synchronized void stopQuery() throws TorqueException 726 { 727 if (log.isDebugEnabled()) 728 { 729 log.debug("stopQuery(): Stopping query thread"); 730 } 731 if (threadRunning) 732 { 733 killThread = true; 734 while (thread.isAlive()) 735 { 736 try 737 { 738 Thread.sleep(QUERY_STOP_SLEEP_TIME); 739 } 740 catch (InterruptedException e) 741 { 742 throw new TorqueException("Unexpected interruption", e); 743 } 744 } 745 killThread = false; 746 if (log.isDebugEnabled()) 747 { 748 log.debug("stopQuery(): query thread stopped."); 749 } 750 } 751 } 752 753 /** 754 * Retrieve the number of the current page. 755 * 756 * @return the current page number. 757 */ 758 public int getCurrentPageNumber() 759 { 760 return position / pageSize; 761 } 762 763 /** 764 * Retrieve the total number of search result records that are known to 765 * exist (this will be the actual value when the query has completeted (see 766 * <code>getTotalsFinalized()</code>). The convenience method 767 * <code>getRecordProgressText()</code> may be more useful for presenting to 768 * users. 769 * 770 * @return the number of result records known to exist (not accurate until 771 * <code>getTotalsFinalized()</code> returns <code>true</code>). 772 */ 773 public int getTotalRecords() 774 { 775 return totalRecords; 776 } 777 778 /** 779 * Provide an indication of whether or not paging of results will be 780 * required. 781 * 782 * @return <code>true</code> when multiple pages of results exist. 783 */ 784 public boolean getPaginated() 785 { 786 // Handle a page memory limit of 1 page. 787 if (!getTotalsFinalized()) 788 { 789 return true; 790 } 791 return blockBegin + currentlyFilledTo + 1 > pageSize; 792 } 793 794 /** 795 * Retrieve the total number of pages of search results that are known to 796 * exist (this will be the actual value when the query has completeted (see 797 * <code>getQyeryCompleted()</code>). The convenience method 798 * <code>getPageProgressText()</code> may be more useful for presenting to 799 * users. 800 * 801 * @return the number of pages of results known to exist (not accurate until 802 * <code>getTotalsFinalized()</code> returns <code>true</code>). 803 */ 804 public int getTotalPages() 805 { 806 if (totalPages > -1) 807 { 808 return totalPages; 809 } 810 811 int tempPageCount = getTotalRecords() / pageSize 812 + (getTotalRecords() % pageSize > 0 ? 1 : 0); 813 814 if (getTotalsFinalized()) 815 { 816 totalPages = tempPageCount; 817 } 818 819 return tempPageCount; 820 } 821 822 /** 823 * Retrieve the page size. 824 * 825 * @return the number of records returned on each invocation of 826 * <code>getNextResults()</code>/<code>getPreviousResults()</code>. 827 */ 828 public int getPageSize() 829 { 830 return pageSize; 831 } 832 833 /** 834 * Provide access to indicator that the total values for the number of 835 * records and pages are now accurate as opposed to known upper limits. 836 * 837 * @return <code>true</code> when the totals are known to have been fully 838 * computed. 839 */ 840 public boolean getTotalsFinalized() 841 { 842 return totalsFinalized; 843 } 844 845 /** 846 * Provide a way of changing the more pages/records indicator. 847 * 848 * @param moreIndicator the indicator to use in place of the default 849 * (">"). 850 */ 851 public void setMoreIndicator(String moreIndicator) 852 { 853 this.moreIndicator = moreIndicator; 854 } 855 856 /** 857 * Retrieve the more pages/records indicator. 858 */ 859 public String getMoreIndicator() 860 { 861 return this.moreIndicator; 862 } 863 864 /** 865 * Sets the multiplier that will be used to compute the memory limit when a 866 * constructor with no memory page limit is used - the memory limit will be 867 * this number multiplied by the page size. 868 * 869 * @param memoryPageLimit the maximum number of pages to be in memory 870 * at one time. 871 */ 872 public void setMemoryPageLimit(int memoryPageLimit) 873 { 874 this.memoryPageLimit = memoryPageLimit; 875 } 876 877 /** 878 * Retrieves the multiplier that will be used to compute the memory limit 879 * when a constructor with no memory page limit is used - the memory limit 880 * will be this number multiplied by the page size. 881 */ 882 public int getMemoryPageLimit() 883 { 884 return this.memoryPageLimit; 885 } 886 887 /** 888 * A convenience method that provides text showing progress through the 889 * selected rows on a page basis. 890 * 891 * @return progress text in the form of "1 of > 5" where ">" can be 892 * configured using <code>setMoreIndicator()</code>. 893 */ 894 public String getPageProgressText() 895 { 896 StringBuffer result = new StringBuffer(); 897 result.append(getCurrentPageNumber()); 898 result.append(" of "); 899 if (!totalsFinalized) 900 { 901 result.append(moreIndicator); 902 result.append(" "); 903 } 904 result.append(getTotalPages()); 905 return result.toString(); 906 } 907 908 /** 909 * Provides a count of the number of rows to be displayed on the current 910 * page - for the last page this may be less than the configured page size. 911 * 912 * @return the number of records that are included on the current page of 913 * results. 914 * @throws TorqueException if invoking the <code>populateObjects()<code> 915 * method runs into problems or a sleep is unexpectedly interrupted. 916 */ 917 public int getCurrentPageSize() throws TorqueException 918 { 919 if (null == getCurrentPageResults()) 920 { 921 return 0; 922 } 923 return getCurrentPageResults().size(); 924 } 925 926 /** 927 * Provide the record number of the first row included on the current page. 928 * 929 * @return The record number of the first row of the current page. 930 */ 931 public int getFirstRecordNoForPage() 932 { 933 if (getCurrentPageNumber() < 1) 934 { 935 return 0; 936 } 937 return (getCurrentPageNumber() - 1) * getPageSize() + 1; 938 } 939 940 /** 941 * Provide the record number of the last row included on the current page. 942 * 943 * @return the record number of the last row of the current page. 944 * @throws TorqueException if invoking the <code>populateObjects()<code> 945 * method runs into problems or a sleep is unexpectedly interrupted. 946 */ 947 public int getLastRecordNoForPage() throws TorqueException 948 { 949 if (0 == getCurrentPageNumber()) 950 { 951 return 0; 952 } 953 return (getCurrentPageNumber() - 1) * getPageSize() 954 + getCurrentPageSize(); 955 } 956 957 /** 958 * A convenience method that provides text showing progress through the 959 * selected rows on a record basis. 960 * 961 * @return progress text in the form of "26 - 50 of > 250" where ">" 962 * can be configured using <code>setMoreIndicator()</code>. 963 * @throws TorqueException if invoking the <code>populateObjects()<code> 964 * method runs into problems or a sleep is unexpectedly interrupted. 965 */ 966 public String getRecordProgressText() throws TorqueException 967 { 968 StringBuffer result = new StringBuffer(); 969 result.append(getFirstRecordNoForPage()); 970 result.append(" - "); 971 result.append(getLastRecordNoForPage()); 972 result.append(" of "); 973 if (!totalsFinalized) 974 { 975 result.append(moreIndicator); 976 result.append(" "); 977 } 978 result.append(getTotalRecords()); 979 return result.toString(); 980 } 981 982 /** 983 * Indicates if further result pages are available. 984 * 985 * @return <code>true</code> when further results are available. 986 */ 987 public boolean getNextResultsAvailable() 988 { 989 if (!totalsFinalized || getCurrentPageNumber() < getTotalPages()) 990 { 991 return true; 992 } 993 return false; 994 } 995 996 /** 997 * Indicates if previous results pages are available. 998 * 999 * @return <code>true</code> when previous results are available. 1000 */ 1001 public boolean getPreviousResultsAvailable() 1002 { 1003 if (getCurrentPageNumber() <= 1) 1004 { 1005 return false; 1006 } 1007 return true; 1008 } 1009 1010 /** 1011 * Indicates if any results are available. 1012 * 1013 * @return <code>true</code> of any results are available. 1014 */ 1015 public boolean hasResultsAvailable() 1016 { 1017 return getTotalRecords() > 0; 1018 } 1019 1020 /** 1021 * Clear the query result so that the query is reexecuted when the next page 1022 * is retrieved. You may want to invoke this method if you are returning to 1023 * a page after performing an operation on an item in the result set. 1024 * 1025 * @throws TorqueException if a sleep is interrupted. 1026 */ 1027 public synchronized void invalidateResult() throws TorqueException 1028 { 1029 stopQuery(); 1030 blockBegin = 0; 1031 blockEnd = 0; 1032 currentlyFilledTo = -1; 1033 results = null; 1034 // TODO Perhaps store the oldPosition and immediately restart the 1035 // query. 1036 // oldPosition = position; 1037 position = 0; 1038 totalPages = -1; 1039 totalRecords = 0; 1040 queryCompleted = false; 1041 totalsFinalized = false; 1042 lastResults = null; 1043 } 1044 1045 /** 1046 * Retrieve a search parameter. This acts as a convenient place to store 1047 * parameters that relate to the LargeSelect to make it easy to get at them 1048 * in order to repopulate search parameters on a form when the next page of 1049 * results is retrieved - they in no way effect the operation of 1050 * LargeSelect. 1051 * 1052 * @param name the search parameter key to retrieve. 1053 * @return the value of the search parameter. 1054 */ 1055 public String getSearchParam(String name) 1056 { 1057 return getSearchParam(name, null); 1058 } 1059 1060 /** 1061 * Retrieve a search parameter. This acts as a convenient place to store 1062 * parameters that relate to the LargeSelect to make it easy to get at them 1063 * in order to repopulate search parameters on a form when the next page of 1064 * results is retrieved - they in no way effect the operation of 1065 * LargeSelect. 1066 * 1067 * @param name the search parameter key to retrieve. 1068 * @param defaultValue the default value to return if the key is not found. 1069 * @return the value of the search parameter. 1070 */ 1071 public String getSearchParam(String name, String defaultValue) 1072 { 1073 if (null == params) 1074 { 1075 return defaultValue; 1076 } 1077 String value = params.get(name); 1078 return null == value ? defaultValue : value; 1079 } 1080 1081 /** 1082 * Set a search parameter. If the value is <code>null</code> then the 1083 * key will be removed from the parameters. 1084 * 1085 * @param name the search parameter key to set. 1086 * @param value the value of the search parameter to store. 1087 */ 1088 public void setSearchParam(String name, String value) 1089 { 1090 if (null == value) 1091 { 1092 removeSearchParam(name); 1093 } 1094 else 1095 { 1096 if (null != name) 1097 { 1098 if (null == params) 1099 { 1100 params = new Hashtable<String, String>(); 1101 } 1102 params.put(name, value); 1103 } 1104 } 1105 } 1106 1107 /** 1108 * Remove a value from the search parameters. 1109 * 1110 * @param name the search parameter key to remove. 1111 */ 1112 public void removeSearchParam(String name) 1113 { 1114 if (null != params) 1115 { 1116 params.remove(name); 1117 } 1118 } 1119 1120 /** 1121 * Deserialize this LargeSelect instance. 1122 * 1123 * @param inputStream The serialization input stream. 1124 * @throws IOException 1125 * @throws ClassNotFoundException 1126 */ 1127 private void readObject(ObjectInputStream inputStream) 1128 throws IOException, ClassNotFoundException 1129 { 1130 inputStream.defaultReadObject(); 1131 1132 // avoid NPE because of Tomcat de-serialization of sessions 1133 if (Torque.isInit()) 1134 { 1135 startQuery(pageSize); 1136 } 1137 } 1138 1139 /** 1140 * Provide something useful for debugging purposes. 1141 * 1142 * @return some basic information about this instance of LargeSelect. 1143 */ 1144 @Override 1145 public String toString() 1146 { 1147 StringBuffer result = new StringBuffer(); 1148 result.append("LargeSelect - TotalRecords: "); 1149 result.append(getTotalRecords()); 1150 result.append(" TotalsFinalised: "); 1151 result.append(getTotalsFinalized()); 1152 result.append("\nParameters:"); 1153 if (null == params || params.size() == 0) 1154 { 1155 result.append(" No parameters have been set."); 1156 } 1157 else 1158 { 1159 Set<String> keys = params.keySet(); 1160 for (Iterator<String> iter = keys.iterator(); iter.hasNext();) 1161 { 1162 String key = iter.next(); 1163 String val = params.get(key); 1164 result.append("\n ").append(key).append(": ").append(val); 1165 } 1166 } 1167 return result.toString(); 1168 } 1169 1170 }