1 package org.apache.torque.util;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 (">") used to indicate that the total number of
208 * records or pages is unknown.
209 */
210 public static final String DEFAULT_MORE_INDICATOR = ">";
211
212 /***
213 * The value used to indicate that the total number of records or pages is
214 * unknown (default: ">"). 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
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 > 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
570
571
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
595
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
619 return getResults(start, size);
620 }
621
622
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
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
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
700
701 return;
702 }
703
704 int size;
705 if (dbSupportsNativeLimit && dbSupportsNativeOffset)
706 {
707
708 size = pageSize;
709 }
710 else
711 {
712
713
714
715 size = blockBegin + memoryLimit + 1;
716 }
717
718 Connection conn = null;
719 /*** Used to retrieve query results from Village. */
720 QueryDataSet qds = null;
721
722 try
723 {
724
725 results = new ArrayList(memoryLimit + 1);
726
727
728
729 if (dbSupportsNativeLimit)
730 {
731 if (dbSupportsNativeOffset)
732 {
733 criteria.setOffset(blockBegin);
734
735
736 criteria.setLimit(memoryLimit + 1);
737 }
738 else
739 {
740 criteria.setLimit(blockBegin + memoryLimit + 1);
741 }
742 }
743
744
745
746
747
748
749
750 BasePeer.correctBooleans(criteria, null);
751
752 query = BasePeer.createQueryString(criteria);
753
754
755 conn = Torque.getConnection(dbName);
756
757
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
768
769
770 while (!killThread
771 && !qds.allRecordsRetrieved()
772 && currentlyFilledTo + pageSize <= blockEnd)
773 {
774
775
776
777
778 if ((currentlyFilledTo + pageSize) >= blockEnd
779 && dbSupportsNativeLimit)
780 {
781
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
816
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
833 totalRecords = blockBegin + currentlyFilledTo + 1;
834 }
835
836
837
838
839 if (qds.allRecordsRetrieved()
840 || !dbSupportsNativeLimit)
841 {
842 queryCompleted = true;
843
844
845
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
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 * (">").
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 > 5" where ">" 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 > 250" where ">"
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
1229
1230
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
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 }