0001: package org.apache.torque.util;
0002:
0003: /*
0004: * Licensed to the Apache Software Foundation (ASF) under one
0005: * or more contributor license agreements. See the NOTICE file
0006: * distributed with this work for additional information
0007: * regarding copyright ownership. The ASF licenses this file
0008: * to you under the Apache License, Version 2.0 (the
0009: * "License"); you may not use this file except in compliance
0010: * with the License. You may obtain a copy of the License at
0011: *
0012: * http://www.apache.org/licenses/LICENSE-2.0
0013: *
0014: * Unless required by applicable law or agreed to in writing,
0015: * software distributed under the License is distributed on an
0016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
0017: * KIND, either express or implied. See the License for the
0018: * specific language governing permissions and limitations
0019: * under the License.
0020: */
0021:
0022: import java.io.IOException;
0023: import java.io.ObjectInputStream;
0024: import java.io.Serializable;
0025: import java.lang.reflect.Method;
0026: import java.sql.Connection;
0027: import java.sql.SQLException;
0028: import java.util.ArrayList;
0029: import java.util.Hashtable;
0030: import java.util.Iterator;
0031: import java.util.List;
0032: import java.util.Set;
0033:
0034: import org.apache.commons.logging.Log;
0035: import org.apache.commons.logging.LogFactory;
0036: import org.apache.torque.Torque;
0037: import org.apache.torque.TorqueException;
0038:
0039: import com.workingdogs.village.DataSetException;
0040: import com.workingdogs.village.QueryDataSet;
0041:
0042: /**
0043: * This class can be used to retrieve a large result set from a database query.
0044: * The query is started and then rows are returned a page at a time. The <code>
0045: * LargeSelect</code> is meant to be placed into the Session or User.Temp, so
0046: * that it can be used in response to several related requests. Note that in
0047: * order to use <code>LargeSelect</code> you need to be willing to accept the
0048: * fact that the result set may become inconsistent with the database if updates
0049: * are processed subsequent to the queries being executed. Specifying a memory
0050: * page limit of 1 will give you a consistent view of the records but the totals
0051: * may not be accurate and the performance will be terrible. In most cases
0052: * the potential for inconsistencies data should not cause any serious problems
0053: * and performance should be pretty good (but read on for further warnings).
0054: *
0055: * <p>The idea here is that the full query result would consume too much memory
0056: * and if displayed to a user the page would be too long to be useful. Rather
0057: * than loading the full result set into memory, a window of data (the memory
0058: * limit) is loaded and retrieved a page at a time. If a request occurs for
0059: * data that falls outside the currently loaded window of data then a new query
0060: * is executed to fetch the required data. Performance is optimized by
0061: * starting a thread to execute the database query and fetch the results. This
0062: * will perform best when paging forwards through the data, but a minor
0063: * optimization where the window is moved backwards by two rather than one page
0064: * is included for when a user pages past the beginning of the window.
0065: *
0066: * <p>As the query is performed in in steps, it is often the case that the total
0067: * number of records and pages of data is unknown. <code>LargeSelect</code>
0068: * provides various methods for indicating how many records and pages it is
0069: * currently aware of and for presenting this information to users.
0070: *
0071: * <p><code>LargeSelect</code> utilises the <code>Criteria</code> methods
0072: * <code>setOffset()</code> and <code>setLimit()</code> to limit the amount of
0073: * data retrieved from the database - these values are either passed through to
0074: * the DBMS when supported (efficient with the caveat below) or handled by
0075: * the Village API when it is not (not so efficient). At time of writing
0076: * <code>Criteria</code> will only pass the offset and limit through to MySQL
0077: * and PostgreSQL (with a few changes to <code>DBOracle</code> and <code>
0078: * BasePeer</code> Oracle support can be implemented by utilising the <code>
0079: * rownum</code> pseudo column).
0080: *
0081: * <p>As <code>LargeSelect</code> must re-execute the query each time the user
0082: * pages out of the window of loaded data, you should consider the impact of
0083: * non-index sort orderings and other criteria that will require the DBMS to
0084: * execute the entire query before filtering down to the offset and limit either
0085: * internally or via Village.
0086: *
0087: * <p>The memory limit defaults to 5 times the page size you specify, but
0088: * alternative constructors and the class method <code>setMemoryPageLimit()
0089: * </code> allow you to override this for a specific instance of
0090: * <code>LargeSelect</code> or future instances respectively.
0091: *
0092: * <p>Some of the constructors allow you to specify the name of the class to use
0093: * to build the returnd rows. This works by using reflection to find <code>
0094: * addSelectColumns(Criteria)</code> and <code>populateObjects(List)</code>
0095: * methods to add the necessary select columns to the criteria (only if it
0096: * doesn't already contain any) and to convert query results from Village
0097: * <code>Record</code> objects to a class defined within the builder class.
0098: * This allows you to use any of the Torque generated Peer classes, but also
0099: * makes it fairly simple to construct business object classes that can be used
0100: * for this purpose (simply copy and customise the <code>addSelectColumns()
0101: * </code>, <code>populateObjects()</code>, <code>row2Object()</code> and <code>
0102: * populateObject()</code> methods from an existing Peer class).
0103: *
0104: * <p>Typically you will create a <code>LargeSelect</code> using your <code>
0105: * Criteria</code> (perhaps created from the results of a search parameter
0106: * page), page size, memory page limit and return class name (for which you may
0107: * have defined a business object class before hand) and place this in user.Temp
0108: * thus:
0109: *
0110: * <pre>
0111: * data.getUser().setTemp("someName", largeSelect);
0112: * </pre>
0113: *
0114: * <p>In your template you will then use something along the lines of:
0115: *
0116: * <pre>
0117: * #set($largeSelect = $data.User.getTemp("someName"))
0118: * #set($searchop = $data.Parameters.getString("searchop"))
0119: * #if($searchop.equals("prev"))
0120: * #set($recs = $largeSelect.PreviousResults)
0121: * #else
0122: * #if($searchop.equals("goto"))
0123: * #set($recs = $largeSelect.getPage($data.Parameters.getInt("page", 1)))
0124: * #else
0125: * #set($recs = $largeSelect.NextResults)
0126: * #end
0127: * #end
0128: * </pre>
0129: *
0130: * <p>...to move through the records. <code>LargeSelect</code> implements a
0131: * number of convenience methods that make it easy to add all of the necessary
0132: * bells and whistles to your template.
0133: *
0134: * @author <a href="mailto:john.mcnally@clearink.com">John D. McNally</a>
0135: * @author <a href="mailto:seade@backstagetech.com.au">Scott Eade</a>
0136: * @version $Id: LargeSelect.java 534001 2007-05-01 10:46:05Z tv $
0137: */
0138: public class LargeSelect implements Runnable, Serializable {
0139: /** Serial version */
0140: private static final long serialVersionUID = -1166842932571491942L;
0141:
0142: /** The number of records that a page consists of. */
0143: private int pageSize;
0144: /** The maximum number of records to maintain in memory. */
0145: private int memoryLimit;
0146:
0147: /** The record number of the first record in memory. */
0148: private transient int blockBegin = 0;
0149: /** The record number of the last record in memory. */
0150: private transient int blockEnd;
0151: /** How much of the memory block is currently occupied with result data. */
0152: private volatile int currentlyFilledTo = -1;
0153:
0154: /** The SQL query that this <code>LargeSelect</code> represents. */
0155: private String query;
0156: /** The database name to get from Torque. */
0157: private String dbName;
0158:
0159: /** The memory store of records. */
0160: private transient List results = null;
0161:
0162: /** The thread that executes the query. */
0163: private transient Thread thread = null;
0164: /**
0165: * A flag used to kill the thread when the currently executing query is no
0166: * longer required.
0167: */
0168: private transient volatile boolean killThread = false;
0169: /** A flag that indicates whether or not the query thread is running. */
0170: private transient volatile boolean threadRunning = false;
0171: /**
0172: * An indication of whether or not the current query has completed
0173: * processing.
0174: */
0175: private transient volatile boolean queryCompleted = false;
0176: /**
0177: * An indication of whether or not the totals (records and pages) are at
0178: * their final values.
0179: */
0180: private transient boolean totalsFinalized = false;
0181:
0182: /** The cursor position in the result set. */
0183: private int position;
0184: /** The total number of pages known to exist. */
0185: private int totalPages = -1;
0186: /** The total number of records known to exist. */
0187: private int totalRecords = 0;
0188:
0189: /** The criteria used for the query. */
0190: private Criteria criteria = null;
0191: /** The last page of results that were returned. */
0192: private transient List lastResults;
0193:
0194: /**
0195: * The class that is possibly used to construct the criteria and used
0196: * to transform the Village Records into the desired OM or business objects.
0197: */
0198: private Class returnBuilderClass = null;
0199: /**
0200: * A reference to the method in the return builder class that will
0201: * convert the Village Records to the desired class.
0202: */
0203: private transient Method populateObjectsMethod = null;
0204:
0205: /**
0206: * The default value (">") used to indicate that the total number of
0207: * records or pages is unknown.
0208: */
0209: public static final String DEFAULT_MORE_INDICATOR = ">";
0210:
0211: /**
0212: * The value used to indicate that the total number of records or pages is
0213: * unknown (default: ">"). You can use <code>setMoreIndicator()</code>
0214: * to change this to whatever value you like (e.g. "more than").
0215: */
0216: private static String moreIndicator = DEFAULT_MORE_INDICATOR;
0217:
0218: /**
0219: * The default value for the maximum number of pages of data to be retained
0220: * in memory.
0221: */
0222: public static final int DEFAULT_MEMORY_LIMIT_PAGES = 5;
0223:
0224: /**
0225: * The maximum number of pages of data to be retained in memory. Use
0226: * <code>setMemoryPageLimit()</code> to provide your own value.
0227: */
0228: private static int memoryPageLimit = DEFAULT_MEMORY_LIMIT_PAGES;
0229:
0230: /**
0231: * The number of milliseconds to sleep when the result of a query
0232: * is not yet available.
0233: */
0234: private static final int QUERY_NOT_COMPLETED_SLEEP_TIME = 500;
0235:
0236: /**
0237: * The number of milliseconds to sleep before retrying to stop a query.
0238: */
0239: private static final int QUERY_STOP_SLEEP_TIME = 100;
0240:
0241: /** A place to store search parameters that relate to this query. */
0242: private Hashtable params = null;
0243:
0244: /** Logging */
0245: private static Log log = LogFactory.getLog(LargeSelect.class);
0246:
0247: /**
0248: * Creates a LargeSelect whose results are returned as a <code>List</code>
0249: * containing a maximum of <code>pageSize</code> Village <code>Record</code>
0250: * objects at a time, maintaining a maximum of
0251: * <code>LargeSelect.memoryPageLimit</code> pages of results in memory.
0252: *
0253: * @param criteria object used by BasePeer to build the query. In order to
0254: * allow this class to utilise database server implemented offsets and
0255: * limits (when available), the provided criteria must not have any limit or
0256: * offset defined.
0257: * @param pageSize number of rows to return in one block.
0258: * @throws IllegalArgumentException if <code>criteria</code> uses one or
0259: * both of offset and limit, or if <code>pageSize</code> is less than 1;
0260: */
0261: public LargeSelect(Criteria criteria, int pageSize) {
0262: this (criteria, pageSize, LargeSelect.memoryPageLimit);
0263: }
0264:
0265: /**
0266: * Creates a LargeSelect whose results are returned as a <code>List</code>
0267: * containing a maximum of <code>pageSize</code> Village <code>Record</code>
0268: * objects at a time, maintaining a maximum of <code>memoryPageLimit</code>
0269: * pages of results in memory.
0270: *
0271: * @param criteria object used by BasePeer to build the query. In order to
0272: * allow this class to utilise database server implemented offsets and
0273: * limits (when available), the provided criteria must not have any limit or
0274: * offset defined.
0275: * @param pageSize number of rows to return in one block.
0276: * @param memoryPageLimit maximum number of pages worth of rows to be held
0277: * in memory at one time.
0278: * @throws IllegalArgumentException if <code>criteria</code> uses one or
0279: * both of offset and limit, or if <code>pageSize</code> or
0280: * <code>memoryLimitPages</code> are less than 1;
0281: */
0282: public LargeSelect(Criteria criteria, int pageSize,
0283: int memoryPageLimit) {
0284: init(criteria, pageSize, memoryPageLimit);
0285: }
0286:
0287: /**
0288: * Creates a LargeSelect whose results are returned as a <code>List</code>
0289: * containing a maximum of <code>pageSize</code> objects of the type
0290: * defined within the class named <code>returnBuilderClassName</code> at a
0291: * time, maintaining a maximum of <code>LargeSelect.memoryPageLimit</code>
0292: * pages of results in memory.
0293: *
0294: * @param criteria object used by BasePeer to build the query. In order to
0295: * allow this class to utilise database server implemented offsets and
0296: * limits (when available), the provided criteria must not have any limit or
0297: * offset defined. If the criteria does not include the definition of any
0298: * select columns the <code>addSelectColumns(Criteria)</code> method of
0299: * the class named as <code>returnBuilderClassName</code> will be used to
0300: * add them.
0301: * @param pageSize number of rows to return in one block.
0302: * @param returnBuilderClassName The name of the class that will be used to
0303: * build the result records (may implement <code>addSelectColumns(Criteria)
0304: * </code> and must implement <code>populateObjects(List)</code>).
0305: * @throws IllegalArgumentException if <code>criteria</code> uses one or
0306: * both of offset and limit, if <code>pageSize</code> is less than 1, or if
0307: * problems are experienced locating and invoking either one or both of
0308: * <code>addSelectColumns(Criteria)</code> and <code> populateObjects(List)
0309: * </code> in the class named <code>returnBuilderClassName</code>.
0310: */
0311: public LargeSelect(Criteria criteria, int pageSize,
0312: String returnBuilderClassName) {
0313: this (criteria, pageSize, LargeSelect.memoryPageLimit,
0314: returnBuilderClassName);
0315: }
0316:
0317: /**
0318: * Creates a LargeSelect whose results are returned as a <code>List</code>
0319: * containing a maximum of <code>pageSize</code> objects of the type
0320: * defined within the class named <code>returnBuilderClassName</code> at a
0321: * time, maintaining a maximum of <code>memoryPageLimit</code> pages of
0322: * results in memory.
0323: *
0324: * @param criteria object used by BasePeer to build the query. In order to
0325: * allow this class to utilise database server implemented offsets and
0326: * limits (when available), the provided criteria must not have any limit or
0327: * offset defined. If the criteria does not include the definition of any
0328: * select columns the <code>addSelectColumns(Criteria)</code> method of
0329: * the class named as <code>returnBuilderClassName</code> will be used to
0330: * add them.
0331: * @param pageSize number of rows to return in one block.
0332: * @param memoryPageLimit maximum number of pages worth of rows to be held
0333: * in memory at one time.
0334: * @param returnBuilderClassName The name of the class that will be used to
0335: * build the result records (may implement <code>addSelectColumns(Criteria)
0336: * </code> and must implement <code>populateObjects(List)</code>).
0337: * @throws IllegalArgumentException if <code>criteria</code> uses one or
0338: * both of offset and limit, if <code>pageSize</code> or <code>
0339: * memoryLimitPages</code> are less than 1, or if problems are experienced
0340: * locating and invoking either one or both of <code>
0341: * addSelectColumns(Criteria)</code> and <code> populateObjects(List)</code>
0342: * in the class named <code>returnBuilderClassName</code>.
0343: */
0344: public LargeSelect(Criteria criteria, int pageSize,
0345: int memoryPageLimit, String returnBuilderClassName) {
0346: try {
0347: this .returnBuilderClass = Class
0348: .forName(returnBuilderClassName);
0349:
0350: // Add the select columns if necessary.
0351: if (criteria.getSelectColumns().size() == 0) {
0352: Class[] argTypes = { Criteria.class };
0353: Method selectColumnAdder = returnBuilderClass
0354: .getMethod("addSelectColumns", argTypes);
0355: Object[] theArgs = { criteria };
0356: selectColumnAdder.invoke(returnBuilderClass
0357: .newInstance(), theArgs);
0358: }
0359: } catch (Exception e) {
0360: throw new IllegalArgumentException(
0361: "The class named as returnBuilderClassName does not "
0362: + "provide the necessary facilities - see javadoc.");
0363: }
0364:
0365: init(criteria, pageSize, memoryPageLimit);
0366: }
0367:
0368: /**
0369: * Access the populateObjects method.
0370: *
0371: * @throws SecurityException if the security manager does not allow
0372: * access to the method.
0373: * @throws NoSuchMethodException if the poulateObjects method does not
0374: * exist.
0375: */
0376: private Method getPopulateObjectsMethod()
0377: throws NoSuchMethodException {
0378: if (null == populateObjectsMethod) {
0379: Class[] argTypes = { List.class };
0380: populateObjectsMethod = returnBuilderClass.getMethod(
0381: "populateObjects", argTypes);
0382: }
0383: return populateObjectsMethod;
0384: }
0385:
0386: /**
0387: * Called by the constructors to start the query.
0388: *
0389: * @param criteria Object used by <code>BasePeer</code> to build the query.
0390: * In order to allow this class to utilise database server implemented
0391: * offsets and limits (when available), the provided criteria must not have
0392: * any limit or offset defined.
0393: * @param pageSize number of rows to return in one block.
0394: * @param memoryLimitPages maximum number of pages worth of rows to be held
0395: * in memory at one time.
0396: * @throws IllegalArgumentException if <code>criteria</code> uses one or
0397: * both of offset and limit and if <code>pageSize</code> or
0398: * <code>memoryLimitPages</code> are less than 1;
0399: */
0400: private void init(Criteria criteria, int pageSize,
0401: int memoryLimitPages) {
0402: if (criteria.getOffset() != 0 || criteria.getLimit() != -1) {
0403: throw new IllegalArgumentException(
0404: "criteria must not use Offset and/or Limit.");
0405: }
0406:
0407: if (pageSize < 1) {
0408: throw new IllegalArgumentException(
0409: "pageSize must be greater than zero.");
0410: }
0411:
0412: if (memoryLimitPages < 1) {
0413: throw new IllegalArgumentException(
0414: "memoryPageLimit must be greater than zero.");
0415: }
0416:
0417: this .pageSize = pageSize;
0418: this .memoryLimit = pageSize * memoryLimitPages;
0419: this .criteria = criteria;
0420: dbName = criteria.getDbName();
0421: blockEnd = blockBegin + memoryLimit - 1;
0422: startQuery(pageSize);
0423: }
0424:
0425: /**
0426: * Retrieve a specific page, if it exists.
0427: *
0428: * @param pageNumber the number of the page to be retrieved - must be
0429: * greater than zero. An empty <code>List</code> will be returned if
0430: * <code>pageNumber</code> exceeds the total number of pages that exist.
0431: * @return a <code>List</code> of query results containing a maximum of
0432: * <code>pageSize</code> results.
0433: * @throws IllegalArgumentException when <code>pageNo</code> is not
0434: * greater than zero.
0435: * @throws TorqueException if invoking the <code>populateObjects()<code>
0436: * method runs into problems or a sleep is unexpectedly interrupted.
0437: */
0438: public List getPage(int pageNumber) throws TorqueException {
0439: if (pageNumber < 1) {
0440: throw new IllegalArgumentException(
0441: "pageNumber must be greater than zero.");
0442: }
0443: return getResults((pageNumber - 1) * pageSize);
0444: }
0445:
0446: /**
0447: * Gets the next page of rows.
0448: *
0449: * @return a <code>List</code> of query results containing a maximum of
0450: * <code>pageSize</code> reslts.
0451: * @throws TorqueException if invoking the <code>populateObjects()<code>
0452: * method runs into problems or a sleep is unexpectedly interrupted.
0453: */
0454: public List getNextResults() throws TorqueException {
0455: if (!getNextResultsAvailable()) {
0456: return getCurrentPageResults();
0457: }
0458: return getResults(position);
0459: }
0460:
0461: /**
0462: * Provide access to the results from the current page.
0463: *
0464: * @return a <code>List</code> of query results containing a maximum of
0465: * <code>pageSize</code> reslts.
0466: * @throws TorqueException if invoking the <code>populateObjects()<code>
0467: * method runs into problems or a sleep is unexpectedly interrupted.
0468: */
0469: public List getCurrentPageResults() throws TorqueException {
0470: return null == lastResults && position > 0 ? getResults(position)
0471: : lastResults;
0472: }
0473:
0474: /**
0475: * Gets the previous page of rows.
0476: *
0477: * @return a <code>List</code> of query results containing a maximum of
0478: * <code>pageSize</code> reslts.
0479: * @throws TorqueException if invoking the <code>populateObjects()<code>
0480: * method runs into problems or a sleep is unexpectedly interrupted.
0481: */
0482: public List getPreviousResults() throws TorqueException {
0483: if (!getPreviousResultsAvailable()) {
0484: return getCurrentPageResults();
0485: }
0486:
0487: int start;
0488: if (position - 2 * pageSize < 0) {
0489: start = 0;
0490: } else {
0491: start = position - 2 * pageSize;
0492: }
0493: return getResults(start);
0494: }
0495:
0496: /**
0497: * Gets a page of rows starting at a specified row.
0498: *
0499: * @param start the starting row.
0500: * @return a <code>List</code> of query results containing a maximum of
0501: * <code>pageSize</code> reslts.
0502: * @throws TorqueException if invoking the <code>populateObjects()<code>
0503: * method runs into problems or a sleep is unexpectedly interrupted.
0504: */
0505: private List getResults(int start) throws TorqueException {
0506: return getResults(start, pageSize);
0507: }
0508:
0509: /**
0510: * Gets a block of rows starting at a specified row and containing a
0511: * specified number of rows.
0512: *
0513: * @param start the starting row.
0514: * @param size the number of rows.
0515: * @return a <code>List</code> of query results containing a maximum of
0516: * <code>pageSize</code> reslts.
0517: * @throws IllegalArgumentException if <code>size > memoryLimit</code> or
0518: * <code>start</code> and <code>size</code> result in a situation that is
0519: * not catered for.
0520: * @throws TorqueException if invoking the <code>populateObjects()<code>
0521: * method runs into problems or a sleep is unexpectedly interrupted.
0522: */
0523: private synchronized List getResults(int start, int size)
0524: throws TorqueException {
0525: if (log.isDebugEnabled()) {
0526: log.debug("getResults(start: " + start + ", size: " + size
0527: + ") invoked.");
0528: }
0529:
0530: if (size > memoryLimit) {
0531: throw new IllegalArgumentException("size (" + size
0532: + ") exceeds memory limit (" + memoryLimit + ").");
0533: }
0534:
0535: // Request was for a block of rows which should be in progess.
0536: // If the rows have not yet been returned, wait for them to be
0537: // retrieved.
0538: if (start >= blockBegin && (start + size - 1) <= blockEnd) {
0539: if (log.isDebugEnabled()) {
0540: log.debug("getResults(): Sleeping until "
0541: + "start+size-1 (" + (start + size - 1)
0542: + ") > currentlyFilledTo (" + currentlyFilledTo
0543: + ") && !queryCompleted (!" + queryCompleted
0544: + ")");
0545: }
0546: while (((start + size - 1) > currentlyFilledTo)
0547: && !queryCompleted) {
0548: try {
0549: Thread.sleep(QUERY_NOT_COMPLETED_SLEEP_TIME);
0550: } catch (InterruptedException e) {
0551: throw new TorqueException(
0552: "Unexpected interruption", e);
0553: }
0554: }
0555: }
0556:
0557: // Going in reverse direction, trying to limit db hits so assume user
0558: // might want at least 2 sets of data.
0559: else if (start < blockBegin && start >= 0) {
0560: if (log.isDebugEnabled()) {
0561: log.debug("getResults(): Paging backwards as start ("
0562: + start + ") < blockBegin (" + blockBegin
0563: + ") && start >= 0");
0564: }
0565: stopQuery();
0566: if (memoryLimit >= 2 * size) {
0567: blockBegin = start - size;
0568: if (blockBegin < 0) {
0569: blockBegin = 0;
0570: }
0571: } else {
0572: blockBegin = start;
0573: }
0574: blockEnd = blockBegin + memoryLimit - 1;
0575: startQuery(size);
0576: // Re-invoke getResults() to provide the wait processing.
0577: return getResults(start, size);
0578: }
0579:
0580: // Assume we are moving on, do not retrieve any records prior to start.
0581: else if ((start + size - 1) > blockEnd) {
0582: if (log.isDebugEnabled()) {
0583: log
0584: .debug("getResults(): Paging past end of loaded data as "
0585: + "start+size-1 ("
0586: + (start + size - 1)
0587: + ") > blockEnd (" + blockEnd + ")");
0588: }
0589: stopQuery();
0590: blockBegin = start;
0591: blockEnd = blockBegin + memoryLimit - 1;
0592: startQuery(size);
0593: // Re-invoke getResults() to provide the wait processing.
0594: return getResults(start, size);
0595: }
0596:
0597: else {
0598: throw new IllegalArgumentException(
0599: "Parameter configuration not " + "accounted for.");
0600: }
0601:
0602: int fromIndex = start - blockBegin;
0603: int toIndex = fromIndex
0604: + Math.min(size, results.size() - fromIndex);
0605:
0606: if (log.isDebugEnabled()) {
0607: log
0608: .debug("getResults(): Retrieving records from results elements "
0609: + "start-blockBegin ("
0610: + fromIndex
0611: + ") through "
0612: + "fromIndex + Math.min(size, results.size() - fromIndex) ("
0613: + toIndex + ")");
0614: }
0615:
0616: List returnResults;
0617:
0618: synchronized (results) {
0619: returnResults = new ArrayList(results.subList(fromIndex,
0620: toIndex));
0621: }
0622:
0623: if (null != returnBuilderClass) {
0624: // Invoke the populateObjects() method
0625: Object[] theArgs = { returnResults };
0626: try {
0627: returnResults = (List) getPopulateObjectsMethod()
0628: .invoke(returnBuilderClass.newInstance(),
0629: theArgs);
0630: } catch (Exception e) {
0631: throw new TorqueException("Unable to populate results",
0632: e);
0633: }
0634: }
0635: position = start + size;
0636: lastResults = returnResults;
0637: return returnResults;
0638: }
0639:
0640: /**
0641: * A background thread that retrieves the rows.
0642: */
0643: public void run() {
0644: boolean dbSupportsNativeLimit;
0645: boolean dbSupportsNativeOffset;
0646: try {
0647: dbSupportsNativeLimit = (Torque.getDB(dbName)
0648: .supportsNativeLimit());
0649: dbSupportsNativeOffset = (Torque.getDB(dbName)
0650: .supportsNativeOffset());
0651: } catch (TorqueException e) {
0652: log.error("run() : Exiting :", e);
0653: // we cannot execute further because Torque is not initialized
0654: // correctly
0655: return;
0656: }
0657:
0658: int size;
0659: if (dbSupportsNativeLimit && dbSupportsNativeOffset) {
0660: // retrieve one page at a time
0661: size = pageSize;
0662: } else {
0663: // retrieve the whole block at once and add the offset,
0664: // and add one record to check if we have reached the end of the
0665: // data
0666: size = blockBegin + memoryLimit + 1;
0667: }
0668: /* The connection to the database. */
0669: Connection conn = null;
0670: /** Used to retrieve query results from Village. */
0671: QueryDataSet qds = null;
0672:
0673: try {
0674: // Add 1 to memory limit to check if the query ends on a page break.
0675: results = new ArrayList(memoryLimit + 1);
0676:
0677: // Use the criteria to limit the rows that are retrieved to the
0678: // block of records that fit in the predefined memoryLimit.
0679: if (dbSupportsNativeLimit) {
0680: if (dbSupportsNativeOffset) {
0681: criteria.setOffset(blockBegin);
0682: // Add 1 to memory limit to check if the query ends on a
0683: // page break.
0684: criteria.setLimit(memoryLimit + 1);
0685: } else {
0686: criteria.setLimit(blockBegin + memoryLimit + 1);
0687: }
0688: }
0689:
0690: /*
0691: * Fix criterions relating to booleanint or booleanchar columns
0692: * The defaultTableMap parameter in this call is null because we have
0693: * no default peer class inside LargeSelect. This means that all
0694: * columns not fully qualified will not be modified.
0695: */
0696: BasePeer.correctBooleans(criteria, null);
0697:
0698: query = BasePeer.createQueryString(criteria);
0699:
0700: // Get a connection to the db.
0701: conn = Torque.getConnection(dbName);
0702:
0703: // Execute the query.
0704: if (log.isDebugEnabled()) {
0705: log.debug("run(): query = " + query);
0706: log.debug("run(): memoryLimit = " + memoryLimit);
0707: log.debug("run(): blockBegin = " + blockBegin);
0708: log.debug("run(): blockEnd = " + blockEnd);
0709: }
0710: qds = new QueryDataSet(conn, query);
0711:
0712: // Continue getting rows one page at a time until the memory limit
0713: // is reached, all results have been retrieved, or the rest
0714: // of the results have been determined to be irrelevant.
0715: while (!killThread && !qds.allRecordsRetrieved()
0716: && currentlyFilledTo + pageSize <= blockEnd) {
0717: // This caters for when memoryLimit is not a multiple of
0718: // pageSize which it never is because we always add 1 above.
0719: // not applicable if the db has no native limit where this
0720: // was already considered
0721: if ((currentlyFilledTo + pageSize) >= blockEnd
0722: && dbSupportsNativeLimit) {
0723: // Add 1 to check if the query ends on a page break.
0724: size = blockEnd - currentlyFilledTo + 1;
0725: }
0726:
0727: if (log.isDebugEnabled()) {
0728: log
0729: .debug("run(): Invoking BasePeer.getSelectResults(qds, "
0730: + size + ", false)");
0731: }
0732:
0733: List tempResults = BasePeer.getSelectResults(qds, size,
0734: false);
0735:
0736: int startIndex = dbSupportsNativeOffset ? 0
0737: : blockBegin;
0738:
0739: synchronized (results) {
0740: for (int i = startIndex, n = tempResults.size(); i < n; i++) {
0741: results.add(tempResults.get(i));
0742: }
0743: }
0744:
0745: if (dbSupportsNativeLimit && dbSupportsNativeOffset) {
0746: currentlyFilledTo += tempResults.size();
0747: } else {
0748: currentlyFilledTo = tempResults.size() - 1
0749: - blockBegin;
0750: }
0751:
0752: boolean perhapsLastPage = true;
0753:
0754: // If the extra record was indeed found then we know we are not
0755: // on the last page but we must now get rid of it.
0756: if ((dbSupportsNativeLimit && (results.size() == memoryLimit + 1))
0757: || (!dbSupportsNativeLimit && currentlyFilledTo >= memoryLimit)) {
0758: synchronized (results) {
0759: results.remove(currentlyFilledTo--);
0760: }
0761: perhapsLastPage = false;
0762: }
0763:
0764: if (results.size() > 0
0765: && blockBegin + currentlyFilledTo >= totalRecords) {
0766: // Add 1 because index starts at 0
0767: totalRecords = blockBegin + currentlyFilledTo + 1;
0768: }
0769:
0770: // if the db has limited the datasets, we must retrieve all
0771: // datasets. If not, we are always finished because we fetch
0772: // the whole block at once.
0773: if (qds.allRecordsRetrieved() || !dbSupportsNativeLimit) {
0774: queryCompleted = true;
0775: // The following ugly condition ensures that the totals are
0776: // not finalized when a user does something like requesting
0777: // a page greater than what exists in the database.
0778: if (perhapsLastPage
0779: && getCurrentPageNumber() <= getTotalPages()) {
0780: totalsFinalized = true;
0781: }
0782: }
0783: qds.clearRecords();
0784: }
0785:
0786: if (log.isDebugEnabled()) {
0787: log
0788: .debug("run(): While loop terminated because either:");
0789: log.debug("run(): 1. qds.allRecordsRetrieved(): "
0790: + qds.allRecordsRetrieved());
0791: log.debug("run(): 2. killThread: " + killThread);
0792: log
0793: .debug("run(): 3. !(currentlyFilledTo + size <= blockEnd): !"
0794: + (currentlyFilledTo + pageSize <= blockEnd));
0795: log.debug("run(): - currentlyFilledTo: "
0796: + currentlyFilledTo);
0797: log.debug("run(): - size: " + pageSize);
0798: log.debug("run(): - blockEnd: " + blockEnd);
0799: log.debug("run(): - results.size(): " + results.size());
0800: }
0801: } catch (TorqueException e) {
0802: log.error(e);
0803: } catch (SQLException e) {
0804: log.error(e);
0805: } catch (DataSetException e) {
0806: log.error(e);
0807: } finally {
0808: try {
0809: if (qds != null) {
0810: qds.close();
0811: }
0812: Torque.closeConnection(conn);
0813: } catch (SQLException e) {
0814: log.error(e);
0815: } catch (DataSetException e) {
0816: log.error(e);
0817: }
0818: threadRunning = false;
0819: }
0820: }
0821:
0822: /**
0823: * Starts a new thread to retrieve the result set.
0824: *
0825: * @param initialSize the initial size for each block.
0826: */
0827: private synchronized void startQuery(int initialSize) {
0828: if (!threadRunning) {
0829: pageSize = initialSize;
0830: currentlyFilledTo = -1;
0831: queryCompleted = false;
0832: thread = new Thread(this );
0833: thread.start();
0834: threadRunning = true;
0835: }
0836: }
0837:
0838: /**
0839: * Used to stop filling the memory with the current block of results, if it
0840: * has been determined that they are no longer relevant.
0841: *
0842: * @throws TorqueException if a sleep is interrupted.
0843: */
0844: private synchronized void stopQuery() throws TorqueException {
0845: if (threadRunning) {
0846: killThread = true;
0847: while (thread.isAlive()) {
0848: try {
0849: Thread.sleep(QUERY_STOP_SLEEP_TIME);
0850: } catch (InterruptedException e) {
0851: throw new TorqueException(
0852: "Unexpected interruption", e);
0853: }
0854: }
0855: killThread = false;
0856: }
0857: }
0858:
0859: /**
0860: * Retrieve the number of the current page.
0861: *
0862: * @return the current page number.
0863: */
0864: public int getCurrentPageNumber() {
0865: return position / pageSize;
0866: }
0867:
0868: /**
0869: * Retrieve the total number of search result records that are known to
0870: * exist (this will be the actual value when the query has completeted (see
0871: * <code>getTotalsFinalized()</code>). The convenience method
0872: * <code>getRecordProgressText()</code> may be more useful for presenting to
0873: * users.
0874: *
0875: * @return the number of result records known to exist (not accurate until
0876: * <code>getTotalsFinalized()</code> returns <code>true</code>).
0877: */
0878: public int getTotalRecords() {
0879: return totalRecords;
0880: }
0881:
0882: /**
0883: * Provide an indication of whether or not paging of results will be
0884: * required.
0885: *
0886: * @return <code>true</code> when multiple pages of results exist.
0887: */
0888: public boolean getPaginated() {
0889: // Handle a page memory limit of 1 page.
0890: if (!getTotalsFinalized()) {
0891: return true;
0892: }
0893: return blockBegin + currentlyFilledTo + 1 > pageSize;
0894: }
0895:
0896: /**
0897: * Retrieve the total number of pages of search results that are known to
0898: * exist (this will be the actual value when the query has completeted (see
0899: * <code>getQyeryCompleted()</code>). The convenience method
0900: * <code>getPageProgressText()</code> may be more useful for presenting to
0901: * users.
0902: *
0903: * @return the number of pages of results known to exist (not accurate until
0904: * <code>getTotalsFinalized()</code> returns <code>true</code>).
0905: */
0906: public int getTotalPages() {
0907: if (totalPages > -1) {
0908: return totalPages;
0909: }
0910:
0911: int tempPageCount = getTotalRecords() / pageSize
0912: + (getTotalRecords() % pageSize > 0 ? 1 : 0);
0913:
0914: if (getTotalsFinalized()) {
0915: totalPages = tempPageCount;
0916: }
0917:
0918: return tempPageCount;
0919: }
0920:
0921: /**
0922: * Retrieve the page size.
0923: *
0924: * @return the number of records returned on each invocation of
0925: * <code>getNextResults()</code>/<code>getPreviousResults()</code>.
0926: */
0927: public int getPageSize() {
0928: return pageSize;
0929: }
0930:
0931: /**
0932: * Provide access to indicator that the total values for the number of
0933: * records and pages are now accurate as opposed to known upper limits.
0934: *
0935: * @return <code>true</code> when the totals are known to have been fully
0936: * computed.
0937: */
0938: public boolean getTotalsFinalized() {
0939: return totalsFinalized;
0940: }
0941:
0942: /**
0943: * Provide a way of changing the more pages/records indicator.
0944: *
0945: * @param moreIndicator the indicator to use in place of the default
0946: * (">").
0947: */
0948: public static void setMoreIndicator(String moreIndicator) {
0949: LargeSelect.moreIndicator = moreIndicator;
0950: }
0951:
0952: /**
0953: * Retrieve the more pages/records indicator.
0954: */
0955: public static String getMoreIndicator() {
0956: return LargeSelect.moreIndicator;
0957: }
0958:
0959: /**
0960: * Sets the multiplier that will be used to compute the memory limit when a
0961: * constructor with no memory page limit is used - the memory limit will be
0962: * this number multiplied by the page size.
0963: *
0964: * @param memoryPageLimit the maximum number of pages to be in memory
0965: * at one time.
0966: */
0967: public static void setMemoryPageLimit(int memoryPageLimit) {
0968: LargeSelect.memoryPageLimit = memoryPageLimit;
0969: }
0970:
0971: /**
0972: * Retrieves the multiplier that will be used to compute the memory limit
0973: * when a constructor with no memory page limit is used - the memory limit
0974: * will be this number multiplied by the page size.
0975: */
0976: public static int getMemoryPageLimit() {
0977: return LargeSelect.memoryPageLimit;
0978: }
0979:
0980: /**
0981: * A convenience method that provides text showing progress through the
0982: * selected rows on a page basis.
0983: *
0984: * @return progress text in the form of "1 of > 5" where ">" can be
0985: * configured using <code>setMoreIndicator()</code>.
0986: */
0987: public String getPageProgressText() {
0988: StringBuffer result = new StringBuffer();
0989: result.append(getCurrentPageNumber());
0990: result.append(" of ");
0991: if (!totalsFinalized) {
0992: result.append(moreIndicator);
0993: result.append(" ");
0994: }
0995: result.append(getTotalPages());
0996: return result.toString();
0997: }
0998:
0999: /**
1000: * Provides a count of the number of rows to be displayed on the current
1001: * page - for the last page this may be less than the configured page size.
1002: *
1003: * @return the number of records that are included on the current page of
1004: * results.
1005: * @throws TorqueException if invoking the <code>populateObjects()<code>
1006: * method runs into problems or a sleep is unexpectedly interrupted.
1007: */
1008: public int getCurrentPageSize() throws TorqueException {
1009: if (null == getCurrentPageResults()) {
1010: return 0;
1011: }
1012: return getCurrentPageResults().size();
1013: }
1014:
1015: /**
1016: * Provide the record number of the first row included on the current page.
1017: *
1018: * @return The record number of the first row of the current page.
1019: */
1020: public int getFirstRecordNoForPage() {
1021: if (getCurrentPageNumber() < 1) {
1022: return 0;
1023: }
1024: return (getCurrentPageNumber() - 1) * getPageSize() + 1;
1025: }
1026:
1027: /**
1028: * Provide the record number of the last row included on the current page.
1029: *
1030: * @return the record number of the last row of the current page.
1031: * @throws TorqueException if invoking the <code>populateObjects()<code>
1032: * method runs into problems or a sleep is unexpectedly interrupted.
1033: */
1034: public int getLastRecordNoForPage() throws TorqueException {
1035: if (0 == getCurrentPageNumber()) {
1036: return 0;
1037: }
1038: return (getCurrentPageNumber() - 1) * getPageSize()
1039: + getCurrentPageSize();
1040: }
1041:
1042: /**
1043: * A convenience method that provides text showing progress through the
1044: * selected rows on a record basis.
1045: *
1046: * @return progress text in the form of "26 - 50 of > 250" where ">"
1047: * can be configured using <code>setMoreIndicator()</code>.
1048: * @throws TorqueException if invoking the <code>populateObjects()<code>
1049: * method runs into problems or a sleep is unexpectedly interrupted.
1050: */
1051: public String getRecordProgressText() throws TorqueException {
1052: StringBuffer result = new StringBuffer();
1053: result.append(getFirstRecordNoForPage());
1054: result.append(" - ");
1055: result.append(getLastRecordNoForPage());
1056: result.append(" of ");
1057: if (!totalsFinalized) {
1058: result.append(moreIndicator);
1059: result.append(" ");
1060: }
1061: result.append(getTotalRecords());
1062: return result.toString();
1063: }
1064:
1065: /**
1066: * Indicates if further result pages are available.
1067: *
1068: * @return <code>true</code> when further results are available.
1069: */
1070: public boolean getNextResultsAvailable() {
1071: if (!totalsFinalized
1072: || getCurrentPageNumber() < getTotalPages()) {
1073: return true;
1074: }
1075: return false;
1076: }
1077:
1078: /**
1079: * Indicates if previous results pages are available.
1080: *
1081: * @return <code>true</code> when previous results are available.
1082: */
1083: public boolean getPreviousResultsAvailable() {
1084: if (getCurrentPageNumber() <= 1) {
1085: return false;
1086: }
1087: return true;
1088: }
1089:
1090: /**
1091: * Indicates if any results are available.
1092: *
1093: * @return <code>true</code> of any results are available.
1094: */
1095: public boolean hasResultsAvailable() {
1096: return getTotalRecords() > 0;
1097: }
1098:
1099: /**
1100: * Clear the query result so that the query is reexecuted when the next page
1101: * is retrieved. You may want to invoke this method if you are returning to
1102: * a page after performing an operation on an item in the result set.
1103: *
1104: * @throws TorqueException if a sleep is interrupted.
1105: */
1106: public synchronized void invalidateResult() throws TorqueException {
1107: stopQuery();
1108: blockBegin = 0;
1109: blockEnd = 0;
1110: currentlyFilledTo = -1;
1111: results = null;
1112: // TODO Perhaps store the oldPosition and immediately restart the
1113: // query.
1114: // oldPosition = position;
1115: position = 0;
1116: totalPages = -1;
1117: totalRecords = 0;
1118: queryCompleted = false;
1119: totalsFinalized = false;
1120: lastResults = null;
1121: }
1122:
1123: /**
1124: * Retrieve a search parameter. This acts as a convenient place to store
1125: * parameters that relate to the LargeSelect to make it easy to get at them
1126: * in order to repopulate search parameters on a form when the next page of
1127: * results is retrieved - they in no way effect the operation of
1128: * LargeSelect.
1129: *
1130: * @param name the search parameter key to retrieve.
1131: * @return the value of the search parameter.
1132: */
1133: public String getSearchParam(String name) {
1134: return getSearchParam(name, null);
1135: }
1136:
1137: /**
1138: * Retrieve a search parameter. This acts as a convenient place to store
1139: * parameters that relate to the LargeSelect to make it easy to get at them
1140: * in order to repopulate search parameters on a form when the next page of
1141: * results is retrieved - they in no way effect the operation of
1142: * LargeSelect.
1143: *
1144: * @param name the search parameter key to retrieve.
1145: * @param defaultValue the default value to return if the key is not found.
1146: * @return the value of the search parameter.
1147: */
1148: public String getSearchParam(String name, String defaultValue) {
1149: if (null == params) {
1150: return defaultValue;
1151: }
1152: String value = (String) params.get(name);
1153: return null == value ? defaultValue : value;
1154: }
1155:
1156: /**
1157: * Set a search parameter. If the value is <code>null</code> then the
1158: * key will be removed from the parameters.
1159: *
1160: * @param name the search parameter key to set.
1161: * @param value the value of the search parameter to store.
1162: */
1163: public void setSearchParam(String name, String value) {
1164: if (null == value) {
1165: removeSearchParam(name);
1166: } else {
1167: if (null != name) {
1168: if (null == params) {
1169: params = new Hashtable();
1170: }
1171: params.put(name, value);
1172: }
1173: }
1174: }
1175:
1176: /**
1177: * Remove a value from the search parameters.
1178: *
1179: * @param name the search parameter key to remove.
1180: */
1181: public void removeSearchParam(String name) {
1182: if (null != params) {
1183: params.remove(name);
1184: }
1185: }
1186:
1187: /**
1188: * Deserialize this LargeSelect instance.
1189: *
1190: * @param inputStream The serialization input stream.
1191: * @throws IOException
1192: * @throws ClassNotFoundException
1193: */
1194: private void readObject(ObjectInputStream inputStream)
1195: throws IOException, ClassNotFoundException {
1196: inputStream.defaultReadObject();
1197:
1198: // avoid NPE because of Tomcat de-serialization of sessions
1199: if (Torque.isInit()) {
1200: startQuery(pageSize);
1201: }
1202: }
1203:
1204: /**
1205: * Provide something useful for debugging purposes.
1206: *
1207: * @return some basic information about this instance of LargeSelect.
1208: */
1209: public String toString() {
1210: StringBuffer result = new StringBuffer();
1211: result.append("LargeSelect - TotalRecords: ");
1212: result.append(getTotalRecords());
1213: result.append(" TotalsFinalised: ");
1214: result.append(getTotalsFinalized());
1215: result.append("\nParameters:");
1216: if (null == params || params.size() == 0) {
1217: result.append(" No parameters have been set.");
1218: } else {
1219: Set keys = params.keySet();
1220: for (Iterator iter = keys.iterator(); iter.hasNext();) {
1221: String key = (String) iter.next();
1222: String val = (String) params.get(key);
1223: result.append("\n ").append(key).append(": ").append(
1224: val);
1225: }
1226: }
1227: return result.toString();
1228: }
1229:
1230: }
|