View Javadoc

1   /**
2    * Copyright 2005-2013 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.krad.criteria;
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.CriteriaLookupService;
24  import org.kuali.rice.core.api.criteria.CriteriaValue;
25  import org.kuali.rice.core.api.criteria.EqualIgnoreCasePredicate;
26  import org.kuali.rice.core.api.criteria.EqualPredicate;
27  import org.kuali.rice.core.api.criteria.GenericQueryResults;
28  import org.kuali.rice.core.api.criteria.GreaterThanOrEqualPredicate;
29  import org.kuali.rice.core.api.criteria.GreaterThanPredicate;
30  import org.kuali.rice.core.api.criteria.InIgnoreCasePredicate;
31  import org.kuali.rice.core.api.criteria.InPredicate;
32  import org.kuali.rice.core.api.criteria.LessThanOrEqualPredicate;
33  import org.kuali.rice.core.api.criteria.LessThanPredicate;
34  import org.kuali.rice.core.api.criteria.LikePredicate;
35  import org.kuali.rice.core.api.criteria.LookupCustomizer;
36  import org.kuali.rice.core.api.criteria.MultiValuedPredicate;
37  import org.kuali.rice.core.api.criteria.NotEqualIgnoreCasePredicate;
38  import org.kuali.rice.core.api.criteria.NotEqualPredicate;
39  import org.kuali.rice.core.api.criteria.NotInIgnoreCasePredicate;
40  import org.kuali.rice.core.api.criteria.NotInPredicate;
41  import org.kuali.rice.core.api.criteria.NotLikePredicate;
42  import org.kuali.rice.core.api.criteria.NotNullPredicate;
43  import org.kuali.rice.core.api.criteria.NullPredicate;
44  import org.kuali.rice.core.api.criteria.OrPredicate;
45  import org.kuali.rice.core.api.criteria.Predicate;
46  import org.kuali.rice.core.api.criteria.PropertyPathPredicate;
47  import org.kuali.rice.core.api.criteria.QueryByCriteria;
48  import org.kuali.rice.core.api.criteria.SingleValuedPredicate;
49  import org.kuali.rice.core.framework.persistence.jpa.criteria.Criteria;
50  
51  import javax.persistence.EntityManager;
52  import javax.persistence.PersistenceContext;
53  import javax.persistence.Query;
54  import java.sql.Timestamp;
55  import java.util.ArrayList;
56  import java.util.HashSet;
57  import java.util.List;
58  import java.util.Set;
59  
60  public class CriteriaLookupDaoJpa implements CriteriaLookupDao {
61      @PersistenceContext
62  	private EntityManager entityManager;
63  
64      @Override
65      public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria) {
66          return lookup(queryClass, criteria, LookupCustomizer.Builder.<T>create().build());
67      }
68  
69      @Override
70      public <T> GenericQueryResults<T> lookup(final Class<T> queryClass, final QueryByCriteria criteria, LookupCustomizer<T> customizer) {
71          if (queryClass == null) {
72              throw new IllegalArgumentException("queryClass is null");
73          }
74  
75          if (criteria == null) {
76              throw new IllegalArgumentException("criteria is null");
77          }
78  
79          if (customizer == null) {
80              throw new IllegalArgumentException("customizer is null");
81          }
82  
83          final Criteria parent = new Criteria(queryClass.getClass().getName());
84  
85          if (criteria.getPredicate() != null) {
86              addPredicate(criteria.getPredicate(), parent, customizer.getPredicateTransform());
87          }
88  
89          switch (criteria.getCountFlag()) {
90              case ONLY:
91                  return forCountOnly(queryClass, criteria, parent);
92              case NONE:
93                  return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
94              case INCLUDE:
95                  return forRowResults(queryClass, criteria, parent, criteria.getCountFlag(), customizer.getResultTransform());
96              default: throw new UnsupportedCountFlagException(criteria.getCountFlag());
97          }
98      }
99  
100     /** gets results where the actual rows are requested. */
101     private <T> GenericQueryResults<T> forRowResults(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria jpaCriteria, CountFlag flag, LookupCustomizer.Transform<T, T> transform) {
102         final Query jpaQuery = new org.kuali.rice.core.framework.persistence.jpa.criteria.QueryByCriteria(entityManager, jpaCriteria).toQuery();
103         final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create();
104 
105         //ojb's is 1 based, our query api is zero based
106         final int startAtIndex = criteria.getStartAtIndex() != null ? criteria.getStartAtIndex() + 1 : 1;
107         jpaQuery.setFirstResult(startAtIndex);
108 
109         if (criteria.getMaxResults() != null) {
110             //not subtracting one from MaxResults in order to retrieve
111             //one extra row so that the MoreResultsAvailable field can be set
112             jpaQuery.setMaxResults(criteria.getMaxResults());
113         }
114 
115         @SuppressWarnings("unchecked")
116         final List<T> rows = new ArrayList<T>(jpaQuery.getResultList());
117         if (flag == CountFlag.INCLUDE) {
118             results.setTotalRowCount(rows.size());
119         }
120 
121         if (criteria.getMaxResults() != null && rows.size() > criteria.getMaxResults()) {
122             results.setMoreResultsAvailable(true);
123             //remove the extra row that was returned
124             rows.remove(criteria.getMaxResults().intValue());
125         }
126 
127         results.setResults(transformResults(rows, transform));
128         return results.build();
129     }
130 
131     private static <T> List<T> transformResults(List<T> results, LookupCustomizer.Transform<T, T> transform) {
132         final List<T> list = new ArrayList<T>();
133         for (T r : results) {
134             list.add(transform.apply(r));
135         }
136         return list;
137     }
138 
139     /** gets results where only the count is requested. */
140     private <T> GenericQueryResults<T> forCountOnly(final Class<T> queryClass, final QueryByCriteria criteria, final Criteria jpaCriteria) {
141         final Query jpaQuery = new org.kuali.rice.core.framework.persistence.jpa.criteria.QueryByCriteria(entityManager, jpaCriteria).toQuery();
142         final GenericQueryResults.Builder<T> results = GenericQueryResults.Builder.<T>create();
143         // TODO : There has to be a better way to do this.
144         results.setTotalRowCount(jpaQuery.getResultList().size());
145 
146         return results.build();
147     }
148 
149     /** adds a predicate to a Criteria.*/
150     private void addPredicate(Predicate p, Criteria parent, LookupCustomizer.Transform<Predicate, Predicate> transform) {
151         p = transform.apply(p);
152 
153         if (p instanceof PropertyPathPredicate) {
154             final String pp = ((PropertyPathPredicate) p).getPropertyPath();
155             if (p instanceof NotNullPredicate) {
156                 parent.notNull(pp);
157             } else if (p instanceof NullPredicate) {
158                 parent.isNull(pp);
159             } else if (p instanceof SingleValuedPredicate) {
160                 addSingleValuePredicate((SingleValuedPredicate) p, parent);
161             } else if (p instanceof MultiValuedPredicate) {
162                 addMultiValuePredicate((MultiValuedPredicate) p, parent);
163             } else {
164                 throw new UnsupportedPredicateException(p);
165             }
166         } else if (p instanceof CompositePredicate) {
167             addCompositePredicate((CompositePredicate) p, parent, transform);
168         } else {
169             throw new UnsupportedPredicateException(p);
170         }
171     }
172 
173     /** adds a single valued predicate to a Criteria. */
174     private void addSingleValuePredicate(SingleValuedPredicate p, Criteria parent) {
175         final Object value = getVal(p.getValue());
176         final String pp = p.getPropertyPath();
177         if (p instanceof EqualPredicate) {
178             parent.eq(pp, value);
179         } else if (p instanceof EqualIgnoreCasePredicate) {
180             parent.eq(genUpperFunc(pp), ((String) value).toUpperCase());
181         } else if (p instanceof GreaterThanOrEqualPredicate) {
182             parent.gte(pp, value);
183         } else if (p instanceof GreaterThanPredicate) {
184             parent.gt(pp, value);
185         } else if (p instanceof LessThanOrEqualPredicate) {
186             parent.lte(pp, value);
187         } else if (p instanceof LessThanPredicate) {
188             parent.lt(pp, value);
189         } else if (p instanceof LikePredicate) {
190             //no need to convert * or ? since ojb handles the conversion/escaping
191             parent.like(pp, value);
192         } else if (p instanceof NotEqualPredicate) {
193             parent.ne(pp, value);
194         } else if (p instanceof NotEqualIgnoreCasePredicate) {
195             parent.ne(genUpperFunc(pp), ((String) value).toUpperCase());
196         } else if (p instanceof NotLikePredicate) {
197             parent.notLike(pp, value);
198         } else {
199             throw new UnsupportedPredicateException(p);
200         }
201     }
202 
203     /** adds a multi valued predicate to a Criteria. */
204     private void addMultiValuePredicate(MultiValuedPredicate p, Criteria parent) {
205         final String pp = p.getPropertyPath();
206         if (p instanceof InPredicate) {
207             final Set<?> values = getVals(p.getValues());
208             parent.in(pp, values);
209         } else if (p instanceof InIgnoreCasePredicate) {
210             final Set<String> values = toUpper(getValsUnsafe(((InIgnoreCasePredicate) p).getValues()));
211             parent.in(genUpperFunc(pp), values);
212         } else if (p instanceof NotInPredicate) {
213             final Set<?> values = getVals(p.getValues());
214             parent.notIn(pp, values);
215         } else if (p instanceof NotInIgnoreCasePredicate) {
216             final Set<String> values = toUpper(getValsUnsafe(((NotInIgnoreCasePredicate) p).getValues()));
217             parent.notIn(genUpperFunc(pp), values);
218         } else {
219             throw new UnsupportedPredicateException(p);
220         }
221     }
222 
223     /** adds a composite predicate to a Criteria. */
224     private void addCompositePredicate(final CompositePredicate p, final Criteria parent,  LookupCustomizer.Transform<Predicate, Predicate> transform) {
225         for (Predicate ip : p.getPredicates()) {
226             final Criteria inner = new Criteria(parent.getEntityName());
227             addPredicate(ip, inner, transform);
228             if (p instanceof AndPredicate) {
229                 parent.and(inner);
230             } else if (p instanceof OrPredicate) {
231                 parent.or(inner);
232             } else {
233                 throw new UnsupportedPredicateException(p);
234             }
235         }
236     }
237 
238     private static <U extends CriteriaValue<?>> Object getVal(U toConv) {
239         Object o = toConv.getValue();
240         if (o instanceof DateTime) {
241             return new Timestamp(((DateTime) o).getMillis());
242         }
243         return o;
244     }
245 
246     //this is unsafe b/c values could be converted resulting in a classcast exception
247     @SuppressWarnings("unchecked")
248     private static <T, U extends CriteriaValue<T>> Set<T> getValsUnsafe(Set<? extends U> toConv) {
249         return (Set<T>) getVals(toConv);
250     }
251 
252     private static Set<?> getVals(Set<? extends CriteriaValue<?>> toConv) {
253         final Set<Object> values = new HashSet<Object>();
254         for (CriteriaValue<?> value : toConv) {
255             values.add(getVal(value));
256         }
257         return values;
258     }
259 
260     //eliding performance for function composition....
261     private static Set<String> toUpper(Set<String> strs) {
262         final Set<String> values = new HashSet<String>();
263         for (String value : strs) {
264             values.add(value.toUpperCase());
265         }
266         return values;
267     }
268 
269 
270     private String genUpperFunc(String pp) {
271         if (StringUtils.contains(pp, "__JPA_ALIAS[[")) {
272             pp = "UPPER(" + pp + ")";
273         } else {
274             pp = "UPPER(__JPA_ALIAS[[0]]__." + pp + ")";
275         }
276         return pp;
277     }
278 
279     /**
280      * @param entityManager the entityManager to set
281      */
282     public void setEntityManager(EntityManager entityManager) {
283         this.entityManager = entityManager;
284     }
285 
286     /** this is a fatal error since this implementation should support all known predicates. */
287     private static class UnsupportedPredicateException extends RuntimeException {
288         private UnsupportedPredicateException(Predicate predicate) {
289             super("Unsupported predicate [" + String.valueOf(predicate) + "]");
290         }
291     }
292 
293     /** this is a fatal error since this implementation should support all known count flags. */
294     private static class UnsupportedCountFlagException extends RuntimeException {
295         private UnsupportedCountFlagException(CountFlag flag) {
296             super("Unsupported predicate [" + String.valueOf(flag) + "]");
297         }
298     }
299 }