001: /*
002: * Copyright 2004-2007 Gary Bentley
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may
005: * not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015: package org.josql.utils;
016:
017: import java.util.List;
018: import java.util.Comparator;
019:
020: import org.josql.Query;
021: import org.josql.QueryParseException;
022: import org.josql.QueryExecutionException;
023:
024: import org.josql.internal.ListExpressionComparator;
025:
026: /**
027: * This class allows the ORDER BY clause of a JoSQL SQL clause to be used
028: * as a Comparator. It should be noted that is the same as performing: {@link Query#execute(List)}
029: * but there are times when having a separate comparator is desirable.
030: * The EXECUTE ON ALL clause is supported but you must call: {@link #doExecuteOn(List)}
031: * first to ensure that they are executed.
032: * <p>
033: * This class is basically just a thin wrapper around using the comparator gained by
034: * calling: {@link Query#getOrderByComparator()}.
035: * <p>
036: * A note on performance, for small numbers of objects (around 1000) this comparator
037: * has (for vanilla accessors, no function calls) pretty comparable performance against a
038: * hand-coded Java Comparator that performs the same function. However start to scale the
039: * numbers of objects and performance degrades, in testing for ~34000 FileWrapper objects
040: * to order by: <code>path DESC, lastModified, name, length</code> took around: 1300ms.
041: * The hand-coded Java Comparator took around: 180ms! The upshot is, if you need flexibility
042: * and do not need to order large numbers of objects then use this kind of Comparator, if
043: * performance and numbers of objects is an issue then hand-rolling your own Comparator
044: * is probably best. As a side-note, to perform the following order by:
045: * <code>lower(path) DESC, lastModified, name, length</code> using a JoSQLComparator took:
046: * about: 1400ms. However modifying the hand-coded Comparator to use:
047: * {@link String#compareToIgnoreCase(String)} then took about 860ms! And if you using:
048: * {@link String#toLowerCase()} for each string instead, it then takes about: 1800ms!
049: * (Meaning that in certain circumstances JoSQL can be faster!)
050: * <p>
051: * <h3>Caching</h3>
052: * <p>
053: * It is not uncommon for a Comparator (even using the effecient merge-sort implementation of
054: * {@link java.util.Collections#sort(List,Comparator)}) to perform thousands (even millions!)
055: * of comparisons.<br /><br />
056: * However since JoSQL does not automatically cache the results of calls to functions and
057: * results of accessor accesses the performance of this kind of "dynamic" Comparator can
058: * quickly degrade. To mitigate this it is possible to turn "caching" on whereby the
059: * Comparator will "remember" the results of the functions on a per object basis and use those
060: * values instead of calling them again. This is not without it's downside however.
061: * Firstly since a reference to the object will be held it is important (if caching is used
062: * that you call: {@link #clearCache()} once the Comparator has been used to free up those
063: * references (it was considered using a {@link java.util.WeakHashMap} but that doesn't provide
064: * exactly what's needed here).<br /><br />
065: * It is recommended that caching is turned on when the Comparator is to be used in a sort
066: * operation , i.e. calling: {@link java.util.Collections#sort(List,Comparator)} or similar
067: * (however careful consideration needs to be given to the amount of memory that this
068: * may consume, i.e. 4 bytes = 1 object reference, plus 1 List, plus 4 bytes per order
069: * by "column" it soon adds up)<br /><br />
070: * If the comparator is to be used in a {@link java.util.TreeMap} or {@link java.util.TreeSet}
071: * then caching should not be used since the values may (and perhaps should) change over time
072: * but due to caching the order won't change.
073: */
074: public class JoSQLComparator implements Comparator {
075:
076: private Query q = null;
077: private Exception exp = null;
078: private ListExpressionComparator c = null;
079:
080: /**
081: * Execute the EXECUTE ON ALL expressions.
082: *
083: * @param l The list to execute the expressions on.
084: */
085: public void doExecuteOn(List l) throws QueryExecutionException {
086:
087: this .q.doExecuteOn(l, Query.ALL);
088:
089: }
090:
091: /**
092: * Clear the cache, it is VITAL that you call this method before you use
093: * the comparator (if it has been used before) otherwise data objects will
094: * be "left around" and preventing the GC from cleaning them up.
095: */
096: public void clearCache() {
097:
098: if (this .q != null) {
099:
100: this .c.clearCache();
101:
102: }
103:
104: }
105:
106: /**
107: * Return whether this comparator uses caching to improve performance.
108: *
109: * @return <code>true</code> if caching is on.
110: * @throws IllegalStateException If the query has not yet been parsed or set.
111: */
112: public boolean isCaching() throws IllegalStateException {
113:
114: if ((this .q == null) || (!this .q.parsed())) {
115:
116: throw new IllegalStateException(
117: "Query has not yet been parsed.");
118:
119: }
120:
121: return this .c.isCaching();
122:
123: }
124:
125: /**
126: * Set whether the comparator should use caching to improve performance.
127: *
128: * @param b Set to <code>true</code> to turn caching on.
129: * @throws IllegalStateException If the query has not yet been parsed or set.
130: */
131: public void setCaching(boolean b) throws IllegalStateException {
132:
133: if ((this .q == null) || (!this .q.parsed())) {
134:
135: throw new IllegalStateException(
136: "Query has not yet been parsed.");
137:
138: }
139:
140: this .c.setCaching(b);
141:
142: }
143:
144: /**
145: * Init this filter with the query.
146: *
147: * @param q The query.
148: * @throws QueryParseException If there is an issue with the parsing of the query.
149: */
150: public JoSQLComparator(String q) throws QueryParseException {
151:
152: this .setQuery(q);
153:
154: }
155:
156: /**
157: * Compares the objects as according to the ORDER BY clause.
158: *
159: * @param o1 The first object.
160: * @param o2 The second object.
161: */
162: public int compare(Object o1, Object o2) {
163:
164: try {
165:
166: return c.ci(o1, o2);
167:
168: } catch (Exception e) {
169:
170: this .exp = e;
171:
172: return 0;
173:
174: }
175:
176: }
177:
178: /**
179: * Init this file filter with the query already built and parsed.
180: *
181: * @param q The query.
182: * @throws IllegalStateException If the Query object has not been parsed.
183: * @throws QueryParseException If the FROM class is not as expected.
184: */
185: public JoSQLComparator(Query q) throws IllegalStateException,
186: QueryParseException {
187:
188: this .setQuery(q);
189:
190: }
191:
192: /**
193: * The {@link Comparator#compare(Object,Object)} method does not allow for
194: * any exceptions to be thrown however since the execution of the ORDER BY clause
195: * on the objects can cause the throwing of a {@link QueryParseException} it should
196: * be captured. If the exception is thrown then this method will return it.
197: *
198: * @return The exception thrown by the execution of the ORDER BY clause in {@link #compare(Object,Object)}
199: * or by sub-class/interface specific methods, this may be null if no exception was thrown.
200: */
201: public Exception getException() {
202:
203: return this .exp;
204:
205: }
206:
207: /**
208: * Set a new Query (string form) for use in this filter.
209: *
210: * @param q The Query to use.
211: * @throws QueryParseException If there is an issue with the parsing of the query,
212: * or if the FROM class is not as expected.
213: */
214: public void setQuery(String q) throws QueryParseException {
215:
216: this .q = new Query();
217: this .q.parse(q);
218:
219: this .c = (ListExpressionComparator) this .q
220: .getOrderByComparator();
221:
222: this .exp = null;
223:
224: }
225:
226: /**
227: * Set a new Query object for use in this filter.
228: *
229: * @param q The Query to use.
230: * @throws IllegalStateException If the Query object has not been parsed.
231: * @throws QueryParseException If the FROM class is not as expected.
232: */
233: public void setQuery(Query q) throws IllegalStateException,
234: QueryParseException {
235:
236: if (!q.parsed()) {
237:
238: throw new IllegalStateException(
239: "Query has not yet been parsed.");
240:
241: }
242:
243: this .q = q;
244:
245: this .c = (ListExpressionComparator) this .q
246: .getOrderByComparator();
247:
248: this .exp = null;
249:
250: }
251:
252: /**
253: * Get the Query we are using to process objects.
254: *
255: * @return The Query.
256: */
257: public Query getQuery() {
258:
259: return this.q;
260:
261: }
262:
263: }
|