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