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