View Javadoc
1   /**
2    * Copyright 2005-2012 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.student.r2.common.criteria.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.joda.time.DateTime;
20  import org.kuali.rice.core.api.criteria.AndPredicate;
21  import org.kuali.rice.core.api.criteria.CompositePredicate;
22  import org.kuali.rice.core.api.criteria.CountFlag;
23  import org.kuali.rice.core.api.criteria.CriteriaValue;
24  import org.kuali.rice.core.api.criteria.EqualIgnoreCasePredicate;
25  import org.kuali.rice.core.api.criteria.EqualPredicate;
26  import org.kuali.rice.core.api.criteria.GenericQueryResults;
27  import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
28  import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
29  import org.kuali.rice.core.api.criteria.InIgnoreCasePredicate;
30  import org.kuali.rice.core.api.criteria.InPredicate;
31  import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
32  import org.kuali.rice.core.api.criteria.LessThanPredicate;
33  import org.kuali.rice.core.api.criteria.LikePredicate;
34  import org.kuali.rice.core.api.criteria.MultiValuedPredicate;
35  import org.kuali.rice.core.api.criteria.NotEqualIgnoreCasePredicate;
36  import org.kuali.rice.core.api.criteria.NotEqualPredicate;
37  import org.kuali.rice.core.api.criteria.NotInIgnoreCasePredicate;
38  import org.kuali.rice.core.api.criteria.NotInPredicate;
39  import org.kuali.rice.core.api.criteria.NotLikePredicate;
40  import org.kuali.rice.core.api.criteria.NotNullPredicate;
41  import org.kuali.rice.core.api.criteria.NullPredicate;
42  import org.kuali.rice.core.api.criteria.OrPredicate;
43  import org.kuali.rice.core.api.criteria.Predicate;
44  import org.kuali.rice.core.api.criteria.PropertyPathPredicate;
45  import org.kuali.rice.core.api.criteria.QueryByCriteria;
46  import org.kuali.rice.core.api.criteria.SingleValuedPredicate;
47  import org.kuali.student.r2.common.criteria.Criteria;
48  import org.kuali.student.r2.common.criteria.LookupCustomizer;
49  
50  import javax.persistence.EntityManager;
51  import javax.persistence.PersistenceContext;
52  import javax.persistence.Query;
53  import java.sql.Timestamp;
54  import java.util.ArrayList;
55  import java.util.HashSet;
56  import java.util.List;
57  import java.util.Set;
58  
59  /**
60   * 
61   * This class is a copy of the org.kuali.rice.krad.criteria.CriteriaLookupDaoJpa 
62   * implementation in KRAD.
63   * 
64   * It should be removed once these problems are solved.
65   * 
66   * 1) lookup: Replace "new Criteria(queryClass.getClass().getName());" with
67   *  new Criteria(queryClass.getName());
68   *  
69   * 2) forRowResults: jpaQuery.setFirstResult(criteria.getStartAtIndex() != null ? criteria.getStartAtIndex() : 0);
70   * instead of final int startAtIndex = criteria.getStartAtIndex() != null ? criteria.getStartAtIndex() + 1 : 1;
71   * jpaQuery.setFirstResult(startAtIndex);
72   * and
73   * jpaQuery.setMaxResults(criteria.getMaxResults() + 1);
74   * and
75   * rows.remove(criteria.getMaxResults().intValue() + 1);
76   * 
77   * @author Kuali Rice Team (kuali-rice@googlegroups.com)
78   *
79   */
80  public class CriteriaLookupDaoJpaImpl {
81      @PersistenceContext
82      private EntityManager entityManager;
83  
84      public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria, LookupCustomizer<T> customizer) {
85          if (queryClass == null) {
86              throw new IllegalArgumentException("queryClass is null");
87          }
88  
89          if (criteria == null) {
90              throw new IllegalArgumentException("criteria is null");
91          }
92  
93          if (customizer == null) {
94              throw new IllegalArgumentException("customizer is null");
95          }
96  
97          final Criteria parent = new Criteria(queryClass.getName());
98  
99          if (criteria.getPredicate() != null) {
100             Predicate predicate = customizer.applyAdditionalTransforms(criteria.getPredicate(), parent);
101             addPredicate(predicate, parent, parent, customizer);
102         }
103 
104         switch (criteria.getCountFlag()) {
105             case ONLY:
106                 return forCountOnly(queryClass, criteria, parent);
107             case NONE:
108                 return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
109             case INCLUDE:
110                 return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
111             default: throw new UnsupportedCountFlagException(criteria.getCountFlag());
112         }
113     }
114 
115     //Provides lookup for Id of an Entity Object
116     public <T> GenericQueryResults<String> lookupIds(final Class<T> queryClass, final QueryByCriteria criteria, LookupCustomizer<T> customizer) {
117         if (queryClass == null) {
118             throw new IllegalArgumentException("queryClass is null");
119         }
120 
121         if (criteria == null) {
122             throw new IllegalArgumentException("criteria is null");
123         }
124 
125         if (customizer == null) {
126             throw new IllegalArgumentException("customizer is null");
127         }
128 
129         //Create a criteria which doesn't automatically include a Select alias, add a Select for Id
130         Criteria parent = new Criteria(queryClass.getName(), false);
131         parent.select("id");
132 
133 
134         if (criteria.getPredicate() != null) {
135             Predicate predicate = customizer.applyAdditionalTransforms(criteria.getPredicate(), parent);
136             addPredicate(predicate, parent, parent, customizer);
137         }
138 
139         switch (criteria.getCountFlag()) {
140             case ONLY:
141                 return (GenericQueryResults<String>) forCountOnly(queryClass, criteria, parent);
142             case NONE:
143                 return (GenericQueryResults<String>) forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
144             case INCLUDE:
145                 return (GenericQueryResults<String>) forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
146             default: throw new UnsupportedCountFlagException(criteria.getCountFlag());
147         }
148     }
149 
150     //Provides a lookup for any field(s) of an Entity Class.
151     public <T> GenericQueryResults<List<String>> genericLookup(final Class<T> queryClass, final QueryByCriteria criteria, LookupCustomizer<T> customizer, List<String> fields) {
152         if (queryClass == null) {
153             throw new IllegalArgumentException("queryClass is null");
154         }
155 
156         if (criteria == null) {
157             throw new IllegalArgumentException("criteria is null");
158         }
159 
160         if (customizer == null) {
161             throw new IllegalArgumentException("customizer is null");
162         }
163 
164         //Create a criteria which doesn't automatically include a Select alias, add a Select for desired field(s)
165         Criteria parent = new Criteria(queryClass.getName(), false);
166         for(String field: fields){
167             parent.select(field);
168         }
169 
170 
171         if (criteria.getPredicate() != null) {
172             Predicate predicate = customizer.applyAdditionalTransforms(criteria.getPredicate(), parent);
173             addPredicate(predicate, parent, parent, customizer);
174         }
175 
176         switch (criteria.getCountFlag()) {
177             case ONLY:
178                 return  (GenericQueryResults<List<String>>) forCountOnly(queryClass, criteria, parent);
179             case NONE:
180                 return  (GenericQueryResults<List<String>>) forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
181             case INCLUDE:
182                 return  (GenericQueryResults<List<String>>) forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
183             default: throw new UnsupportedCountFlagException(criteria.getCountFlag());
184         }
185     }
186 
187     /** gets results where the actual rows are requested. */
188     private <T> GenericQueryResults<T> forRowResults(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria jpaCriteria, CountFlag flag, LookupCustomizer.Transform<T, T> transform) {
189         final Query jpaQuery = new org.kuali.student.r2.common.criteria.QueryByCriteria(entityManager, jpaCriteria).toQuery();
190         final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create();
191 
192         //ojb's is 1 based, our query api is zero based
193         jpaQuery.setFirstResult(criteria.getStartAtIndex() != null ? criteria.getStartAtIndex() : 0);
194 
195         if (criteria.getMaxResults() != null) {
196             //adding one to MaxResults in order to retrieve
197             //one extra row so that the MoreResultsAvailable field can be set
198             jpaQuery.setMaxResults(criteria.getMaxResults() + 1);
199         }
200 
201         @SuppressWarnings("unchecked")
202         final List<T> rows = new ArrayList<T>(jpaQuery.getResultList());
203         if (flag == CountFlag.INCLUDE) {
204             results.setTotalRowCount(rows.size());
205         }
206 
207         if (criteria.getMaxResults() != null && rows.size() > criteria.getMaxResults()) {
208             results.setMoreResultsAvailable(true);
209             //remove the extra row that was returned
210             rows.remove(criteria.getMaxResults().intValue());
211         }
212 
213         results.setResults(transformResults(rows, transform));
214         return results.build();
215     }
216 
217     private static <T> List<T> transformResults(List<T> results, LookupCustomizer.Transform<T, T> transform) {
218         final List<T> list = new ArrayList<T>();
219         for (T r : results) {
220             list.add(transform.apply(r));
221         }
222         return list;
223     }
224 
225     /** gets results where only the count is requested. */
226     private <T> GenericQueryResults<T> forCountOnly(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria jpaCriteria) {
227         final Query jpaQuery = new org.kuali.student.r2.common.criteria.QueryByCriteria(entityManager, jpaCriteria).toQuery();
228         final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create();
229         // TODO : There has to be a better way to do this.
230         results.setTotalRowCount(jpaQuery.getResultList().size());
231 
232         return results.build();
233     }
234 
235     /** adds a predicate to a Criteria.*/
236     private void addPredicate(Predicate p, Criteria parent, Criteria root, LookupCustomizer<?> customizer) {
237         p = customizer.applyPredicateTransforms(p, root);
238 
239         if (p instanceof PropertyPathPredicate) {
240             final String pp = ((PropertyPathPredicate) p).getPropertyPath();
241             if (p instanceof NotNullPredicate) {
242                 parent.notNull(pp);
243             } else if (p instanceof NullPredicate) {
244                 parent.isNull(pp);
245             } else if (p instanceof SingleValuedPredicate) {
246                 addSingleValuePredicate((SingleValuedPredicate) p, parent);
247             } else if (p instanceof MultiValuedPredicate) {
248                 addMultiValuePredicate((MultiValuedPredicate) p, parent);
249             } else {
250                 throw new UnsupportedPredicateException(p);
251             }
252         } else if (p instanceof CompositePredicate) {
253             addCompositePredicate((CompositePredicate) p, parent, root, customizer);
254         } else {
255             throw new UnsupportedPredicateException(p);
256         }
257     }
258 
259     /** adds a single valued predicate to a Criteria. */
260     private void addSingleValuePredicate(SingleValuedPredicate p, Criteria parent) {
261         final Object value = getVal(p.getValue());
262         final String pp = p.getPropertyPath();
263         if (p instanceof EqualPredicate) {
264             parent.eq(pp, value);
265         } else if (p instanceof EqualIgnoreCasePredicate) {
266             parent.eq(genUpperFunc(pp), ((String) value).toUpperCase());
267         } else if (p instanceof GreaterThanOrEqualPredicate) {
268             parent.gte(pp, value);
269         } else if (p instanceof GreaterThanPredicate) {
270             parent.gt(pp, value);
271         } else if (p instanceof LessThanOrEqualPredicate) {
272             parent.lte(pp, value);
273         } else if (p instanceof LessThanPredicate) {
274             parent.lt(pp, value);
275         } else if (p instanceof LikePredicate) {
276             //no need to convert * or ? since ojb handles the conversion/escaping
277             parent.like(genUpperFunc(pp), ((String) value).toUpperCase());
278         } else if (p instanceof NotEqualPredicate) {
279             parent.ne(pp, value);
280         } else if (p instanceof NotEqualIgnoreCasePredicate) {
281             parent.ne(genUpperFunc(pp), ((String) value).toUpperCase());
282         } else if (p instanceof NotLikePredicate) {
283             parent.notLike(pp, value);
284         } else {
285             throw new UnsupportedPredicateException(p);
286         }
287     }
288 
289     /** adds a multi valued predicate to a Criteria. */
290     private void addMultiValuePredicate(MultiValuedPredicate p, Criteria parent) {
291         final String pp = p.getPropertyPath();
292         if (p instanceof InPredicate) {
293             final Set<?> values = getVals(p.getValues());
294             parent.in(pp, values);
295         } else if (p instanceof InIgnoreCasePredicate) {
296             final Set<String> values = toUpper(getValsUnsafe(((InIgnoreCasePredicate) p).getValues()));
297             parent.in(genUpperFunc(pp), values);
298         } else if (p instanceof NotInPredicate) {
299             final Set<?> values = getVals(p.getValues());
300             parent.notIn(pp, values);
301         } else if (p instanceof NotInIgnoreCasePredicate) {
302             final Set<String> values = toUpper(getValsUnsafe(((NotInIgnoreCasePredicate) p).getValues()));
303             parent.notIn(genUpperFunc(pp), values);
304         } else {
305             throw new UnsupportedPredicateException(p);
306         }
307     }
308 
309     /** adds a composite predicate to a Criteria. */
310     private void addCompositePredicate(final CompositePredicate p, final Criteria parent, Criteria root,  LookupCustomizer<?> customizer) {
311         for (Predicate ip : p.getPredicates()) {
312             final Criteria inner = new Criteria(parent.getEntityName());
313             addPredicate(ip, inner, root, customizer);
314             if (p instanceof AndPredicate) {
315                 parent.and(inner);
316             } else if (p instanceof OrPredicate) {
317                 parent.or(inner);
318             } else {
319                 throw new UnsupportedPredicateException(p);
320             }
321         }
322     }
323 
324     private static <U extends CriteriaValue<?>> Object getVal(U toConv) {
325         Object o = toConv.getValue();
326         if (o instanceof DateTime) {
327             return new Timestamp(((DateTime) o).getMillis());
328         }
329         return o;
330     }
331 
332     //this is unsafe b/c values could be converted resulting in a classcast exception
333     @SuppressWarnings("unchecked")
334     private static <T, U extends CriteriaValue<T>> Set<T> getValsUnsafe(Set<? extends U> toConv) {
335         return (Set<T>) getVals(toConv);
336     }
337 
338     private static Set<?> getVals(Set<? extends CriteriaValue<?>> toConv) {
339         final Set<Object> values = new HashSet<Object>();
340         for (CriteriaValue<?> value : toConv) {
341             values.add(getVal(value));
342         }
343         return values;
344     }
345 
346     //eliding performance for function composition....
347     private static Set<String> toUpper(Set<String> strs) {
348         final Set<String> values = new HashSet<String>();
349         for (String value : strs) {
350             values.add(value.toUpperCase());
351         }
352         return values;
353     }
354 
355 
356     private String genUpperFunc(String pp) {
357         if (StringUtils.contains(pp, "__JPA_ALIAS[[")) {
358             pp = "UPPER(" + pp + ")";
359         } else {
360             pp = "UPPER(__JPA_ALIAS[[0]]__." + pp + ")";
361         }
362         return pp;
363     }
364 
365     /**
366      * @param entityManager the entityManager to set
367      */
368     public void setEntityManager(EntityManager entityManager) {
369         this.entityManager = entityManager;
370     }
371 
372     /** this is a fatal error since this implementation should support all known predicates. */
373     private static class UnsupportedPredicateException extends RuntimeException {
374         private UnsupportedPredicateException(Predicate predicate) {
375             super("Unsupported predicate [" + String.valueOf(predicate) + "]");
376         }
377     }
378 
379     /** this is a fatal error since this implementation should support all known count flags. */
380     private static class UnsupportedCountFlagException extends RuntimeException {
381         private UnsupportedCountFlagException(CountFlag flag) {
382             super("Unsupported predicate [" + String.valueOf(flag) + "]");
383         }
384     }
385 }