View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.core.api.criteria;
17  
18  import java.io.Serializable;
19  import java.lang.reflect.InvocationTargetException;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import javax.xml.bind.annotation.XmlAccessType;
28  import javax.xml.bind.annotation.XmlAccessorType;
29  import javax.xml.bind.annotation.XmlAnyElement;
30  import javax.xml.bind.annotation.XmlElement;
31  import javax.xml.bind.annotation.XmlElementWrapper;
32  import javax.xml.bind.annotation.XmlElements;
33  import javax.xml.bind.annotation.XmlRootElement;
34  import javax.xml.bind.annotation.XmlType;
35  import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
36  
37  import org.apache.commons.beanutils.PropertyUtils;
38  import org.kuali.rice.core.api.CoreConstants;
39  import org.kuali.rice.core.api.mo.AbstractDataTransferObject;
40  import org.kuali.rice.core.api.mo.ModelBuilder;
41  import org.kuali.rice.core.api.util.collect.CollectionUtils;
42  import org.w3c.dom.Element;
43  
44  /**
45   * Defines a criteria-based query.  Consists of a {@link Predicate} definition
46   * as well as a set of additional properties which control paging and other
47   * aspects of the results which should be returned from the query.
48   *
49   * <p>In order to construct a new {@link QueryByCriteria}, the {@link Builder}
50   * should be used.  Use the {@link PredicateFactory} to construct
51   * the predicate for use by the query.
52   *
53   * <p>This class specifies nothing regarding how the query will be executed.
54   * It is expected that an instance will be constructed and then passed to code
55   * which understands how to execute the desired query.
56   *
57   * <p>This class is mapped for use by JAXB and can therefore be used by clients
58   * as part of remotable service definitions.
59   *
60   * @see Predicate
61   * @see PredicateFactory
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   *
65   */
66  @XmlRootElement(name = QueryByCriteria.Constants.ROOT_ELEMENT_NAME)
67  @XmlAccessorType(XmlAccessType.NONE)
68  @XmlType(name = QueryByCriteria.Constants.TYPE_NAME, propOrder = {
69  		QueryByCriteria.Elements.PREDICATE,
70  		QueryByCriteria.Elements.START_AT_INDEX,
71  		QueryByCriteria.Elements.MAX_RESULTS,
72  		QueryByCriteria.Elements.COUNT_FLAG,
73          QueryByCriteria.Elements.ORDER_BY_FIELDS,
74  		CoreConstants.CommonElements.FUTURE_ELEMENTS })
75  public final class QueryByCriteria extends AbstractDataTransferObject {
76  
77  	private static final long serialVersionUID = 2210627777648920180L;
78  
79      @XmlElements(value = {
80          @XmlElement(name = AndPredicate.Constants.ROOT_ELEMENT_NAME, type = AndPredicate.class, required = false),
81          @XmlElement(name = EqualPredicate.Constants.ROOT_ELEMENT_NAME, type = EqualPredicate.class, required = false),
82          @XmlElement(name = EqualIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = EqualIgnoreCasePredicate.class, required = false),
83          @XmlElement(name = ExistsSubQueryPredicate.Constants.ROOT_ELEMENT_NAME, type = ExistsSubQueryPredicate.class, required = false),
84          @XmlElement(name = GreaterThanPredicate.Constants.ROOT_ELEMENT_NAME, type = GreaterThanPredicate.class, required = false),
85          @XmlElement(name = GreaterThanOrEqualPredicate.Constants.ROOT_ELEMENT_NAME, type = GreaterThanOrEqualPredicate.class, required = false),
86          @XmlElement(name = InPredicate.Constants.ROOT_ELEMENT_NAME, type = InPredicate.class, required = false),
87          @XmlElement(name = InIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = InIgnoreCasePredicate.class, required = false),
88          @XmlElement(name = LessThanPredicate.Constants.ROOT_ELEMENT_NAME, type = LessThanPredicate.class, required = false),
89          @XmlElement(name = LessThanOrEqualPredicate.Constants.ROOT_ELEMENT_NAME, type = LessThanOrEqualPredicate.class, required = false),
90          @XmlElement(name = LikePredicate.Constants.ROOT_ELEMENT_NAME, type = LikePredicate.class, required = false),
91          @XmlElement(name = LikeIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = LikeIgnoreCasePredicate.class, required = false),
92          @XmlElement(name = NotEqualPredicate.Constants.ROOT_ELEMENT_NAME, type = NotEqualPredicate.class, required = false),
93          @XmlElement(name = NotEqualIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = NotEqualIgnoreCasePredicate.class, required = false),
94          @XmlElement(name = NotInPredicate.Constants.ROOT_ELEMENT_NAME, type = NotInPredicate.class, required = false),
95          @XmlElement(name = NotInIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = NotInIgnoreCasePredicate.class, required = false),
96          @XmlElement(name = NotLikePredicate.Constants.ROOT_ELEMENT_NAME, type = NotLikePredicate.class, required = false),
97          @XmlElement(name = NotNullPredicate.Constants.ROOT_ELEMENT_NAME, type = NotNullPredicate.class, required = false),
98          @XmlElement(name = NullPredicate.Constants.ROOT_ELEMENT_NAME, type = NullPredicate.class, required = false),
99          @XmlElement(name = OrPredicate.Constants.ROOT_ELEMENT_NAME, type = OrPredicate.class, required = false)
100     })
101 	private final Predicate predicate;
102 
103 	@XmlElement(name = Elements.START_AT_INDEX, required = false)
104 	private final Integer startAtIndex;
105 
106 	@XmlElement(name = Elements.MAX_RESULTS, required = false)
107 	private final Integer maxResults;
108 
109 	@XmlJavaTypeAdapter(CountFlag.Adapter.class)
110 	@XmlElement(name = Elements.COUNT_FLAG, required = true)
111 	private final String countFlag;
112 
113     @XmlElementWrapper(name = Elements.ORDER_BY_FIELDS, required = false)
114     @XmlElement(name = Elements.ORDER_BY_FIELD, required = false)
115     private final List<OrderByField> orderByFields;
116 
117 
118 	@SuppressWarnings("unused")
119 	@XmlAnyElement
120 	private final Collection<Element> _futureElements = null;
121 
122 	private QueryByCriteria() {
123 		this.predicate = null;
124 		this.startAtIndex = null;
125 		this.maxResults = null;
126 		this.countFlag = null;
127         this.orderByFields = null;
128 	}
129 
130 	private QueryByCriteria(Builder builder) {
131 		final Predicate[] preds = builder.predicates;
132         if (preds != null && preds.length > 1) {
133             //implicit "and"
134             this.predicate = PredicateFactory.and(builder.predicates);
135         } else if (preds != null && preds.length == 1) {
136             this.predicate = builder.predicates[0];
137         } else {
138             this.predicate = null;
139         }
140 
141 		this.startAtIndex = builder.getStartAtIndex();
142 		this.maxResults = builder.getMaxResults();
143 		this.countFlag = builder.getCountFlag() == null ? null : builder.getCountFlag().getFlag();
144         this.orderByFields = new ArrayList<OrderByField>(builder.getOrderByFields());
145 	}
146 
147 	/**
148 	 * Returns the {@link Predicate} which will be used to execute the query.
149 	 *
150 	 * @return can be null if no predicate was specified
151 	 */
152 	public Predicate getPredicate() {
153 		return this.predicate;
154 	}
155 
156 	/**
157 	 * Returns the optional zero-based "start" index for rows returned.  When
158 	 * this query is executed, this property should be read to determine the
159 	 * first row which should be returned.  If the given index is beyond the
160 	 * end of the result set, then the resulting query should effectively
161 	 * return no rows (as opposed to producing an index-based out of bounds
162 	 * error).  If the value is null, then the results should start with the
163 	 * first row returned from the query.
164 	 *
165      * <p>
166      * Will never be less than 0
167      *
168 	 * @return the starting row index requested by this query, or null if
169 	 * the results should start at the beginning of the result set
170 	 */
171 	public Integer getStartAtIndex() {
172 		return this.startAtIndex;
173 	}
174 
175 	/**
176 	 * Returns the maximum number of results that this query is requesting
177 	 * to receive.  If null, then the query should return all rows, or as
178 	 * many as it can.  If the number request is larger than the number of
179      * results then all results are returned.
180 	 *
181      * <p>
182      * Will never be less than 0
183      *
184 	 * @return the maximum number of results to return from the query
185 	 */
186 	public Integer getMaxResults() {
187 		return this.maxResults;
188 	}
189 
190 	/**
191 	 * Indicates whether or not a total row count should be returned with the
192 	 * query.  See {@link CountFlag} for more information on what each of these
193 	 * flags means.  This will never return null and defaults to
194 	 * {@link CountFlag#NONE}.
195 	 *
196 	 * @return the flag specifying whether or not a total row count should be
197 	 * produced by the query
198 	 */
199 	public CountFlag getCountFlag() {
200 		return this.countFlag == null ? null : CountFlag.valueOf(this.countFlag);
201 	}
202 
203     /**
204      * Returns the a list of fields that will be ordered depending on the orderDirection
205      * when results are returned.
206      *
207      * @return List of field names that will affect the order of the returned rows
208      */
209     public List<OrderByField> getOrderByFields() {
210         return CollectionUtils.unmodifiableListNullSafe(this.orderByFields);
211     }
212 
213 	public static final class Builder implements ModelBuilder, Serializable {
214 
215 		private Predicate[] predicates;
216 		private Integer startAtIndex;
217 		private Integer maxResults;
218 		private CountFlag countFlag;
219         private List<OrderByField> orderByFields;
220 
221 		private Builder() {
222 			setCountFlag(CountFlag.NONE);
223             setOrderByFields(new ArrayList<OrderByField>());
224 		}
225 
226 		public static Builder create() {
227             return new Builder();
228 		}
229 
230         public static Builder create(QueryByCriteria queryByCriteria) {
231             Builder builder = new Builder();
232             builder.setPredicates(queryByCriteria.getPredicate());
233             builder.setStartAtIndex(queryByCriteria.getStartAtIndex());
234             builder.setMaxResults(queryByCriteria.getMaxResults());
235             builder.setCountFlag(queryByCriteria.getCountFlag());
236             builder.setOrderByFields(queryByCriteria.getOrderByFields());
237             return builder;
238         }
239 
240 		public Integer getStartAtIndex() {
241             return this.startAtIndex;
242 		}
243 
244 		public void setStartAtIndex(Integer startAtIndex) {
245             if (startAtIndex != null && startAtIndex < 0) {
246                 throw new IllegalArgumentException("startAtIndex < 0");
247             }
248 
249             this.startAtIndex = startAtIndex;
250 		}
251 
252 		public Integer getMaxResults() {
253 			return this.maxResults;
254 		}
255 
256 		public void setMaxResults(Integer maxResults) {
257 			if (maxResults != null && maxResults < 0) {
258                 throw new IllegalArgumentException("maxResults < 0");
259             }
260 
261             this.maxResults = maxResults;
262 		}
263 
264 		public CountFlag getCountFlag() {
265 			return this.countFlag;
266 		}
267 
268 		public QueryByCriteria.Builder setCountFlag(CountFlag countFlag) {
269 			if (countFlag == null) {
270                 throw new IllegalArgumentException("countFlag was null");
271             }
272             this.countFlag = countFlag;
273             return this;
274 		}
275 
276         public List<OrderByField> getOrderByFields() {
277             return this.orderByFields;
278         }
279 
280         public QueryByCriteria.Builder setOrderByFields(List<OrderByField> orderByFields) {
281             if (orderByFields == null) {
282                 throw new IllegalArgumentException("orderByFields was null");
283             }
284             this.orderByFields = orderByFields;
285             return this;
286         }
287 
288         public QueryByCriteria.Builder setOrderByFields(OrderByField... orderByFields) {
289             if (orderByFields == null) {
290                 throw new IllegalArgumentException("orderByFields was null");
291             }
292             setOrderByFields(new ArrayList<OrderByField>(Arrays.asList(orderByFields)));
293             return this;
294         }
295 
296         public QueryByCriteria.Builder setOrderByAscending(String... orderByFields) {
297             if (orderByFields == null) {
298                 throw new IllegalArgumentException("orderByFields was null");
299             }
300             List<OrderByField> obf = new ArrayList<OrderByField>(orderByFields.length);
301             for ( String fieldName : orderByFields ) {
302                 obf.add(OrderByField.Builder.create(fieldName, OrderDirection.ASCENDING).build());
303             }
304             setOrderByFields(obf);
305             return this;
306         }
307 
308         /**
309          * will return an array of the predicates.  may return null if no predicates were set.
310          * @return the predicates
311          */
312 		public Predicate[] getPredicates() {
313 			if (this.predicates == null) {
314                 return null;
315             }
316 
317 			//defensive copies on array
318             return Arrays.copyOf(predicates, predicates.length);
319 		}
320 
321         /**
322          * Sets the predicates. If multiple predicates are specified then they are wrapped
323          * in an "and" predicate. If a null predicate is specified then there will be no
324          * constraints on the query.
325          * @param predicates the predicates to set.
326          */
327         public QueryByCriteria.Builder setPredicates(Predicate... predicates) {
328             //defensive copies on array
329             this.predicates = predicates != null ? Arrays.copyOf(predicates, predicates.length) : null;
330             return this;
331 		}
332 
333         @Override
334         public QueryByCriteria build() {
335             return new QueryByCriteria(this);
336         }
337 
338         /** convenience method to create an immutable criteria from one or more predicates. */
339         public static QueryByCriteria fromPredicates(Predicate... predicates) {
340             final Builder b = Builder.create();
341             b.setPredicates(predicates);
342             return b.build();
343         }
344 
345         /** convenience method to create an immutable criteria from one or more predicates. */
346         public static QueryByCriteria fromPredicates(Collection<Predicate> predicates) {
347             final Builder b = Builder.create();
348             if ( predicates != null ) {
349                 b.setPredicates(predicates.toArray(new Predicate[predicates.size()]));
350             } else {
351                 b.setPredicates( (Predicate[])null );
352             }
353             return b.build();
354         }
355 
356         /**
357          * Static helper for generating a QueryByCriteria from a Map<String, ?> of attributes by "OR"-ing those
358          * attributes together.
359          *
360          * @param attributes key/value map of attributes to OR together in the criteria
361          *
362          * @return a QueryByCriteria which selects the given attributes (if map is non-null and non-empty)
363          */
364         public static QueryByCriteria.Builder orAttributes(Map<String, ?> attributes) {
365             List<Predicate> predicates = new ArrayList<Predicate>();
366             if (attributes != null) {
367                 for (Map.Entry<String, ?> entry: attributes.entrySet()) {
368                     if(entry.getValue() instanceof Collection<?>){
369                         for(Object entryVal : (Collection<?>)entry.getValue()) {
370                             predicates.add(buildPredicate(entry.getKey(),entryVal));
371                         }
372                     } else {
373                         predicates.add(buildPredicate(entry.getKey(),entry.getValue()));
374                     }
375                 }
376             }
377             QueryByCriteria.Builder qbc = QueryByCriteria.Builder.create();
378             qbc.setPredicates(PredicateFactory.or(predicates.toArray(new Predicate[predicates.size()])));
379             return qbc;
380         }
381 
382         /**
383          * Static helper for generating a QueryByCriteria from a Map<String, ?> of attributes by "AND"-ing those
384          * attributes together. If any of the values in the Map is a collection, all items in the collection will be
385          * "OR"-ed together (essentially treated like an "IN" condition).
386          *
387          * @param attributes key/value map of attributes to AND together in the criteria
388          *
389          * @return a QueryByCriteria which selects the given attributes (if map is non-null and non-empty)
390          */
391         public static QueryByCriteria.Builder andAttributes(Map<String, ?> attributes) {
392             List<Predicate> predicates = new ArrayList<Predicate>();
393             if (attributes != null) {
394                 for (Map.Entry<String, ?> entry: attributes.entrySet()) {
395                     if(entry.getValue() instanceof Collection<?>) {
396                         Collection<?> values = (Collection<?>)entry.getValue();
397                         if (!values.isEmpty()) {
398                             List<Predicate> orPredicates = new ArrayList<Predicate>();
399                             for(Object entryVal : values) {
400                                 orPredicates.add(buildPredicate(entry.getKey(),entryVal));
401                             }
402                             predicates.add(PredicateFactory.or(orPredicates.toArray(new Predicate[orPredicates.size()])));
403                         }
404                     } else {
405                         predicates.add(buildPredicate(entry.getKey(),entry.getValue()));
406                     }
407                 }
408             }
409             QueryByCriteria.Builder qbc = QueryByCriteria.Builder.create();
410             qbc.setPredicates(PredicateFactory.and(predicates.toArray(new Predicate[predicates.size()])));
411             return qbc;
412         }
413 
414         private static Predicate buildPredicate(String attributeKey, Object attributeValue){
415             if(attributeValue == null){
416                 return PredicateFactory.isNull(attributeKey);
417             } else {
418                 return PredicateFactory.equal(attributeKey,attributeValue);
419             }
420 
421         }
422 
423         /**
424          * Static helper for generating a QueryByCriteria from a single attribute key/value pair
425          * @param name attribute name
426          * @param value attribute value
427          * @return a QueryByCriteria which selects the specified attribute value
428          */
429         public static QueryByCriteria.Builder forAttribute(String name, Object value) {
430             Map<String, Object> attrib = new HashMap<String, Object>();
431             attrib.put(name, value);
432             return andAttributes(attrib);
433         }
434 
435         /**
436          * Static helper for generating a QueryByCriteria that selects the attribute values
437          * that exist on the example object.
438          * @param object the example object
439          * @param attributes list of attributes to select from the example object
440          * @return a QueryByCriteria that selects the attribute values that exist on the example object
441          */
442         public static QueryByCriteria.Builder orAttributes(Object object, Collection<String> attributes) {
443             return orAttributes(getAttributeValueMap(object, attributes));
444         }
445 
446         /**
447          * Static helper for generating a QueryByCriteria that selects the attribute values
448          * that exist on the example object.
449          * @param object the example object
450          * @param attributes list of attributes to select from the example object
451          * @return a QueryByCriteria that selects the attribute values that exist on the example object
452          */
453         public static QueryByCriteria.Builder andAttributes(Object object, Collection<String> attributes) {
454             return andAttributes(getAttributeValueMap(object, attributes));
455         }
456 
457         /**
458          * Uses PropertyUtils to generate a Map of attribute names/values given an example object
459          * and list of attribute names
460          * @param object the object from which to obtain attribute values
461          * @param attribNames the list of attribute names
462          * @return a map of attribute name/value
463          */
464         private static Map<String, ?> getAttributeValueMap(Object object, Collection<String> attribNames) {
465             Map<String, Object> attributeMap = new HashMap<String, Object>();
466             for (String attr: attribNames) {
467                 Object value;
468                 try {
469                     value = PropertyUtils.getProperty(object, attr);
470                 } catch (IllegalAccessException iae) {
471                     throw new RuntimeException(iae);
472                 } catch (InvocationTargetException ite) {
473                     throw new RuntimeException(ite);
474                 } catch (NoSuchMethodException nsme) {
475                     throw new RuntimeException(nsme);
476                 }
477                 attributeMap.put(attr, value);
478             }
479             return attributeMap;
480         }
481     }
482 
483 	/**
484 	 * Defines some internal constants used on this class.
485 	 */
486 	static class Constants {
487 		final static String ROOT_ELEMENT_NAME = "queryByCriteria";
488 		final static String TYPE_NAME = "QueryByCriteriaType";
489 	}
490 
491 	/**
492 	 * A private class which exposes constants which define the XML element
493 	 * names to use when this object is marshaled to XML.
494 	 */
495 	static class Elements {
496 		final static String PREDICATE = "predicate";
497 		final static String START_AT_INDEX = "startAtIndex";
498 		final static String MAX_RESULTS = "maxResults";
499 		final static String COUNT_FLAG = "countFlag";
500         final static String ORDER_BY_FIELDS = "orderByFields";
501         final static String ORDER_BY_FIELD = "orderByField";
502 	}
503 
504 }