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

This page was automatically generated by Maven