View Javadoc

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