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