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 ("&gt;") 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 = "&gt;";
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 &gt; 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      * ("&gt;").
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 &gt; 5" where "&gt;" 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 &gt; 250" where "&gt;"
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 }