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