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 = GreaterThanPredicate.Constants.ROOT_ELEMENT_NAME, type = GreaterThanPredicate.class, required = false),
84          @XmlElement(name = GreaterThanOrEqualPredicate.Constants.ROOT_ELEMENT_NAME, type = GreaterThanOrEqualPredicate.class, required = false),
85          @XmlElement(name = InPredicate.Constants.ROOT_ELEMENT_NAME, type = InPredicate.class, required = false),
86          @XmlElement(name = InIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = InIgnoreCasePredicate.class, required = false),
87          @XmlElement(name = LessThanPredicate.Constants.ROOT_ELEMENT_NAME, type = LessThanPredicate.class, required = false),
88          @XmlElement(name = LessThanOrEqualPredicate.Constants.ROOT_ELEMENT_NAME, type = LessThanOrEqualPredicate.class, required = false),
89          @XmlElement(name = LikePredicate.Constants.ROOT_ELEMENT_NAME, type = LikePredicate.class, required = false),
90          @XmlElement(name = LikeIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = LikeIgnoreCasePredicate.class, required = false),
91          @XmlElement(name = NotEqualPredicate.Constants.ROOT_ELEMENT_NAME, type = NotEqualPredicate.class, required = false),
92          @XmlElement(name = NotEqualIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = NotEqualIgnoreCasePredicate.class, required = false),
93          @XmlElement(name = NotInPredicate.Constants.ROOT_ELEMENT_NAME, type = NotInPredicate.class, required = false),
94          @XmlElement(name = NotInIgnoreCasePredicate.Constants.ROOT_ELEMENT_NAME, type = NotInIgnoreCasePredicate.class, required = false),
95          @XmlElement(name = NotLikePredicate.Constants.ROOT_ELEMENT_NAME, type = NotLikePredicate.class, required = false),
96          @XmlElement(name = NotNullPredicate.Constants.ROOT_ELEMENT_NAME, type = NotNullPredicate.class, required = false),
97          @XmlElement(name = NullPredicate.Constants.ROOT_ELEMENT_NAME, type = NullPredicate.class, required = false),
98          @XmlElement(name = OrPredicate.Constants.ROOT_ELEMENT_NAME, type = OrPredicate.class, required = false)
99      })
100 	private final Predicate predicate;
101 
102 	@XmlElement(name = Elements.START_AT_INDEX, required = false)
103 	private final Integer startAtIndex;
104 
105 	@XmlElement(name = Elements.MAX_RESULTS, required = false)
106 	private final Integer maxResults;
107 
108 	@XmlJavaTypeAdapter(CountFlag.Adapter.class)
109 	@XmlElement(name = Elements.COUNT_FLAG, required = true)
110 	private final String countFlag;
111 
112     @XmlElementWrapper(name = Elements.ORDER_BY_FIELDS, required = false)
113     @XmlElement(name = Elements.ORDER_BY_FIELD, required = false)
114     private final List<OrderByField> orderByFields;
115 
116 
117 	@SuppressWarnings("unused")
118 	@XmlAnyElement
119 	private final Collection<Element> _futureElements = null;
120 
121 	private QueryByCriteria() {
122 		this.predicate = null;
123 		this.startAtIndex = null;
124 		this.maxResults = null;
125 		this.countFlag = null;
126         this.orderByFields = null;
127 	}
128 
129 	private QueryByCriteria(Builder builder) {
130 		final Predicate[] preds = builder.predicates;
131         if (preds != null && preds.length > 1) {
132             //implicit "and"
133             this.predicate = PredicateFactory.and(builder.predicates);
134         } else if (preds != null && preds.length == 1) {
135             this.predicate = builder.predicates[0];
136         } else {
137             this.predicate = null;
138         }
139 
140 		this.startAtIndex = builder.getStartAtIndex();
141 		this.maxResults = builder.getMaxResults();
142 		this.countFlag = builder.getCountFlag() == null ? null : builder.getCountFlag().getFlag();
143         this.orderByFields = new ArrayList<OrderByField>(builder.getOrderByFields());
144 	}
145 
146 	/**
147 	 * Returns the {@link Predicate} which will be used to execute the query.
148 	 *
149 	 * @return can be null if no predicate was specified
150 	 */
151 	public Predicate getPredicate() {
152 		return this.predicate;
153 	}
154 
155 	/**
156 	 * Returns the optional zero-based "start" index for rows returned.  When
157 	 * this query is executed, this property should be read to determine the
158 	 * first row which should be returned.  If the given index is beyond the
159 	 * end of the result set, then the resulting query should effectively
160 	 * return no rows (as opposed to producing an index-based out of bounds
161 	 * error).  If the value is null, then the results should start with the
162 	 * first row returned from the query.
163 	 *
164      * <p>
165      * Will never be less than 0
166      *
167 	 * @return the starting row index requested by this query, or null if
168 	 * the results should start at the beginning of the result set
169 	 */
170 	public Integer getStartAtIndex() {
171 		return this.startAtIndex;
172 	}
173 
174 	/**
175 	 * Returns the maximum number of results that this query is requesting
176 	 * to receive.  If null, then the query should return all rows, or as
177 	 * many as it can.  If the number request is larger than the number of
178      * results then all results are returned.
179 	 *
180      * <p>
181      * Will never be less than 0
182      *
183 	 * @return the maximum number of results to return from the query
184 	 */
185 	public Integer getMaxResults() {
186 		return this.maxResults;
187 	}
188 
189 	/**
190 	 * Indicates whether or not a total row count should be returned with the
191 	 * query.  See {@link CountFlag} for more information on what each of these
192 	 * flags means.  This will never return null and defaults to
193 	 * {@link CountFlag#NONE}.
194 	 *
195 	 * @return the flag specifying whether or not a total row count should be
196 	 * produced by the query
197 	 */
198 	public CountFlag getCountFlag() {
199 		return this.countFlag == null ? null : CountFlag.valueOf(this.countFlag);
200 	}
201 
202     /**
203      * Returns the a list of fields that will be ordered depending on the orderDirection
204      * when results are returned.
205      *
206      * @return List of field names that will affect the order of the returned rows
207      */
208     public List<OrderByField> getOrderByFields() {
209         return CollectionUtils.unmodifiableListNullSafe(this.orderByFields);
210     }
211 
212 	public static final class Builder implements ModelBuilder, Serializable {
213 
214 		private Predicate[] predicates;
215 		private Integer startAtIndex;
216 		private Integer maxResults;
217 		private CountFlag countFlag;
218         private List<OrderByField> orderByFields;
219 
220 		private Builder() {
221 			setCountFlag(CountFlag.NONE);
222             setOrderByFields(new ArrayList<OrderByField>());
223 		}
224 
225 		public static Builder create() {
226             return new Builder();
227 		}
228 
229         public static Builder create(QueryByCriteria queryByCriteria) {
230             Builder builder = new Builder();
231             builder.setPredicates(queryByCriteria.getPredicate());
232             builder.setStartAtIndex(queryByCriteria.getStartAtIndex());
233             builder.setMaxResults(queryByCriteria.getMaxResults());
234             builder.setCountFlag(queryByCriteria.getCountFlag());
235             builder.setOrderByFields(queryByCriteria.getOrderByFields());
236             return builder;
237         }
238 
239 		public Integer getStartAtIndex() {
240             return this.startAtIndex;
241 		}
242 
243 		public void setStartAtIndex(Integer startAtIndex) {
244             if (startAtIndex != null && startAtIndex < 0) {
245                 throw new IllegalArgumentException("startAtIndex < 0");
246             }
247 
248             this.startAtIndex = startAtIndex;
249 		}
250 
251 		public Integer getMaxResults() {
252 			return this.maxResults;
253 		}
254 
255 		public void setMaxResults(Integer maxResults) {
256 			if (maxResults != null && maxResults < 0) {
257                 throw new IllegalArgumentException("maxResults < 0");
258             }
259 
260             this.maxResults = maxResults;
261 		}
262 
263 		public CountFlag getCountFlag() {
264 			return this.countFlag;
265 		}
266 
267 		public QueryByCriteria.Builder setCountFlag(CountFlag countFlag) {
268 			if (countFlag == null) {
269                 throw new IllegalArgumentException("countFlag was null");
270             }
271             this.countFlag = countFlag;
272             return this;
273 		}
274 
275         public List<OrderByField> getOrderByFields() {
276             return this.orderByFields;
277         }
278 
279         public QueryByCriteria.Builder setOrderByFields(List<OrderByField> orderByFields) {
280             if (orderByFields == null) {
281                 throw new IllegalArgumentException("orderByFields was null");
282             }
283             this.orderByFields = orderByFields;
284             return this;
285         }
286 
287         public QueryByCriteria.Builder setOrderByFields(OrderByField... orderByFields) {
288             if (orderByFields == null) {
289                 throw new IllegalArgumentException("orderByFields was null");
290             }
291             setOrderByFields(new ArrayList<OrderByField>(Arrays.asList(orderByFields)));
292             return this;
293         }
294 
295         public QueryByCriteria.Builder setOrderByAscending(String... orderByFields) {
296             if (orderByFields == null) {
297                 throw new IllegalArgumentException("orderByFields was null");
298             }
299             List<OrderByField> obf = new ArrayList<OrderByField>(orderByFields.length);
300             for ( String fieldName : orderByFields ) {
301                 obf.add(OrderByField.Builder.create(fieldName, OrderDirection.ASCENDING).build());
302             }
303             setOrderByFields(obf);
304             return this;
305         }
306 
307         /**
308          * will return an array of the predicates.  may return null if no predicates were set.
309          * @return the predicates
310          */
311 		public Predicate[] getPredicates() {
312 			if (this.predicates == null) {
313                 return null;
314             }
315 
316 			//defensive copies on array
317             return Arrays.copyOf(predicates, predicates.length);
318 		}
319 
320         /**
321          * Sets the predicates. If multiple predicates are specified then they are wrapped
322          * in an "and" predicate. If a null predicate is specified then there will be no
323          * constraints on the query.
324          * @param predicates the predicates to set.
325          */
326         public QueryByCriteria.Builder setPredicates(Predicate... predicates) {
327             //defensive copies on array
328             this.predicates = predicates != null ? Arrays.copyOf(predicates, predicates.length) : null;
329             return this;
330 		}
331 
332         @Override
333         public QueryByCriteria build() {
334             return new QueryByCriteria(this);
335         }
336 
337         /** convenience method to create an immutable criteria from one or more predicates. */
338         public static QueryByCriteria fromPredicates(Predicate... predicates) {
339             final Builder b = Builder.create();
340             b.setPredicates(predicates);
341             return b.build();
342         }
343 
344         /** convenience method to create an immutable criteria from one or more predicates. */
345         public static QueryByCriteria fromPredicates(Collection<Predicate> predicates) {
346             final Builder b = Builder.create();
347             if ( predicates != null ) {
348                 b.setPredicates(predicates.toArray(new Predicate[predicates.size()]));
349             } else {
350                 b.setPredicates( (Predicate[])null );
351             }
352             return b.build();
353         }
354 
355         /**
356          * Static helper for generating a QueryByCriteria from a Map<String, ?> of attributes by "OR"-ing those
357          * attributes together.
358          *
359          * @param attributes key/value map of attributes to OR together in the criteria
360          *
361          * @return a QueryByCriteria which selects the given attributes (if map is non-null and non-empty)
362          */
363         public static QueryByCriteria.Builder orAttributes(Map<String, ?> attributes) {
364             List<Predicate> predicates = new ArrayList<Predicate>();
365             if (attributes != null) {
366                 for (Map.Entry<String, ?> entry: attributes.entrySet()) {
367                     if(entry.getValue() instanceof Collection<?>){
368                         for(Object entryVal : (Collection<?>)entry.getValue()) {
369                             predicates.add(buildPredicate(entry.getKey(),entryVal));
370                         }
371                     } else {
372                         predicates.add(buildPredicate(entry.getKey(),entry.getValue()));
373                     }
374                 }
375             }
376             QueryByCriteria.Builder qbc = QueryByCriteria.Builder.create();
377             qbc.setPredicates(PredicateFactory.or(predicates.toArray(new Predicate[predicates.size()])));
378             return qbc;
379         }
380 
381         /**
382          * Static helper for generating a QueryByCriteria from a Map<String, ?> of attributes by "AND"-ing those
383          * attributes together. If any of the values in the Map is a collection, all items in the collection will be
384          * "OR"-ed together (essentially treated like an "IN" condition).
385          *
386          * @param attributes key/value map of attributes to AND together in the criteria
387          *
388          * @return a QueryByCriteria which selects the given attributes (if map is non-null and non-empty)
389          */
390         public static QueryByCriteria.Builder andAttributes(Map<String, ?> attributes) {
391             List<Predicate> predicates = new ArrayList<Predicate>();
392             if (attributes != null) {
393                 for (Map.Entry<String, ?> entry: attributes.entrySet()) {
394                     if(entry.getValue() instanceof Collection<?>) {
395                         Collection<?> values = (Collection<?>)entry.getValue();
396                         if (!values.isEmpty()) {
397                             List<Predicate> orPredicates = new ArrayList<Predicate>();
398                             for(Object entryVal : values) {
399                                 orPredicates.add(buildPredicate(entry.getKey(),entryVal));
400                             }
401                             predicates.add(PredicateFactory.or(orPredicates.toArray(new Predicate[orPredicates.size()])));
402                         }
403                     } else {
404                         predicates.add(buildPredicate(entry.getKey(),entry.getValue()));
405                     }
406                 }
407             }
408             QueryByCriteria.Builder qbc = QueryByCriteria.Builder.create();
409             qbc.setPredicates(PredicateFactory.and(predicates.toArray(new Predicate[predicates.size()])));
410             return qbc;
411         }
412 
413         private static Predicate buildPredicate(String attributeKey, Object attributeValue){
414             if(attributeValue == null){
415                 return PredicateFactory.isNull(attributeKey);
416             } else {
417                 return PredicateFactory.equal(attributeKey,attributeValue);
418             }
419 
420         }
421 
422         /**
423          * Static helper for generating a QueryByCriteria from a single attribute key/value pair
424          * @param name attribute name
425          * @param value attribute value
426          * @return a QueryByCriteria which selects the specified attribute value
427          */
428         public static QueryByCriteria.Builder forAttribute(String name, Object value) {
429             Map<String, Object> attrib = new HashMap<String, Object>();
430             attrib.put(name, value);
431             return andAttributes(attrib);
432         }
433 
434         /**
435          * Static helper for generating a QueryByCriteria that selects the attribute values
436          * that exist on the example object.
437          * @param object the example object
438          * @param attributes list of attributes to select from the example object
439          * @return a QueryByCriteria that selects the attribute values that exist on the example object
440          */
441         public static QueryByCriteria.Builder orAttributes(Object object, Collection<String> attributes) {
442             return orAttributes(getAttributeValueMap(object, attributes));
443         }
444 
445         /**
446          * Static helper for generating a QueryByCriteria that selects the attribute values
447          * that exist on the example object.
448          * @param object the example object
449          * @param attributes list of attributes to select from the example object
450          * @return a QueryByCriteria that selects the attribute values that exist on the example object
451          */
452         public static QueryByCriteria.Builder andAttributes(Object object, Collection<String> attributes) {
453             return andAttributes(getAttributeValueMap(object, attributes));
454         }
455 
456         /**
457          * Uses PropertyUtils to generate a Map of attribute names/values given an example object
458          * and list of attribute names
459          * @param object the object from which to obtain attribute values
460          * @param attribNames the list of attribute names
461          * @return a map of attribute name/value
462          */
463         private static Map<String, ?> getAttributeValueMap(Object object, Collection<String> attribNames) {
464             Map<String, Object> attributeMap = new HashMap<String, Object>();
465             for (String attr: attribNames) {
466                 Object value;
467                 try {
468                     value = PropertyUtils.getProperty(object, attr);
469                 } catch (IllegalAccessException iae) {
470                     throw new RuntimeException(iae);
471                 } catch (InvocationTargetException ite) {
472                     throw new RuntimeException(ite);
473                 } catch (NoSuchMethodException nsme) {
474                     throw new RuntimeException(nsme);
475                 }
476                 attributeMap.put(attr, value);
477             }
478             return attributeMap;
479         }
480     }
481 
482 	/**
483 	 * Defines some internal constants used on this class.
484 	 */
485 	static class Constants {
486 		final static String ROOT_ELEMENT_NAME = "queryByCriteria";
487 		final static String TYPE_NAME = "QueryByCriteriaType";
488 	}
489 
490 	/**
491 	 * A private class which exposes constants which define the XML element
492 	 * names to use when this object is marshaled to XML.
493 	 */
494 	static class Elements {
495 		final static String PREDICATE = "predicate";
496 		final static String START_AT_INDEX = "startAtIndex";
497 		final static String MAX_RESULTS = "maxResults";
498 		final static String COUNT_FLAG = "countFlag";
499         final static String ORDER_BY_FIELDS = "orderByFields";
500         final static String ORDER_BY_FIELD = "orderByField";
501 	}
502 
503 }