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.krad.data.jpa;
17  
18  import java.util.ArrayList;
19  import java.util.Collection;
20  import java.util.List;
21  
22  import javax.persistence.EntityManager;
23  import javax.persistence.TypedQuery;
24  import javax.persistence.criteria.CriteriaBuilder;
25  import javax.persistence.criteria.CriteriaQuery;
26  import javax.persistence.criteria.Path;
27  import javax.persistence.criteria.Predicate;
28  import javax.persistence.criteria.Root;
29  
30  import org.apache.commons.lang.StringUtils;
31  import org.kuali.rice.core.api.criteria.QueryByCriteria;
32  
33  /**
34   * JPA QueryTranslator that translates queries directly into native JPA 2 Criteria API.
35   *
36   * @author Kuali Rice Team (rice.collab@kuali.org)
37   */
38  class NativeJpaQueryTranslator extends QueryTranslatorBase<NativeJpaQueryTranslator.TranslationContext, TypedQuery> {
39  
40      /**
41       * Wildcard characters that are allowed in queries.
42       */
43  	protected static final String[] LOOKUP_WILDCARDS = { "*", "?" };
44  
45      /**
46       * Wildcard characters that are allowed in queries (in their escape formats).
47       */
48  	protected static final String[] ESCAPED_LOOKUP_WILDCARDS = { "\\*", "\\?" };
49  
50      /**
51       * Wildcard character equivalents in JPQL.
52       */
53  	protected static final char[] JPQL_WILDCARDS = { '%', '_' };
54  
55      /**
56       * The entity manager for interacting with the database.
57       */
58      protected EntityManager entityManager;
59  
60      /**
61       * Thin abstraction/container for criteria parsing context.
62       */
63      public static class TranslationContext {
64  
65          /**
66           * The builder for the criteria.
67           */
68          CriteriaBuilder builder;
69  
70          /**
71           * The query for the criteria.
72           */
73          CriteriaQuery query;
74  
75          /**
76           * The FROM clause root type.
77           */
78          Root root;
79  
80          /**
81           * The list of predicates.
82           */
83          List<Predicate> predicates = new ArrayList<Predicate>();
84  
85          /**
86           * Creates a new criteria parsing context.
87           *
88           * @param entityManager the entity manager to use for interacting with the database.
89           * @param queryClass the type of the query.
90           */
91          TranslationContext(EntityManager entityManager, Class queryClass) {
92              builder = entityManager.getCriteriaBuilder();
93              query = builder.createQuery(queryClass);
94              // establish single consistent root instance
95              // we need this or erroneous query against distinct table aliases is generated
96              root = query.from(query.getResultType());
97          }
98  
99          /**
100          * Creates a new criteria parsing context that is a container for the inner predicates.
101          *
102          * @param parent the parent criteria parsing context.
103          */
104         TranslationContext(TranslationContext parent) {
105             builder = parent.builder;
106             query = parent.query;
107             root = parent.root;
108         }
109 
110         /**
111          * Adds a predicate.
112          *
113          * @param predicate the predicate to add.
114          */
115         void addPredicate(Predicate predicate) {
116             predicates.add(predicate);
117         }
118 
119         /**
120          * Adds an AND clause.
121          *
122          * @param predicate the predicate to AND.
123          */
124         void and(TranslationContext predicate) {
125             addPredicate(predicate.getCriteriaPredicate());
126         }
127 
128         /**
129          * Adds an OR clause.
130          *
131          * @param predicate the predicate to OR.
132          */
133         void or(TranslationContext predicate) {
134             List<Predicate> newpredicates = new ArrayList<Predicate>();
135             //When traversed to a simple OR predicate you may not have a criteria predicate set so check and just
136             //add to builder if necKradEclipseLinkEntityManagerFactoryessary
137             Predicate criteriaPredicate = getCriteriaPredicate();
138             Predicate orPredicate = null;
139             if(criteriaPredicate != null){
140                 orPredicate = builder.or(new Predicate[] {  predicate.getCriteriaPredicate(), getCriteriaPredicate() });
141             } else {
142                 orPredicate = builder.or(predicate.getCriteriaPredicate());
143             }
144             newpredicates.add(orPredicate);
145             predicates = newpredicates;
146         }
147 
148         /**
149          * Gets the criteria predicate.
150          *
151          * @return the criteria predicate.
152          */
153         Predicate getCriteriaPredicate() {
154             if (predicates.size() == 1) {
155                 return predicates.get(0);
156             } else if(predicates.size() > 1){
157                 return builder.and(predicates.toArray(new Predicate[predicates.size()]));
158             } else {
159                 return null;
160             }
161         }
162 
163         /**
164          * Gets the path for the given attribute.
165          *
166          * @param attr the attribute path.
167          * @return the path for the given attribute.
168          */
169         Path attr(String attr) {
170             if (StringUtils.isBlank(attr)) {
171                 throw new IllegalArgumentException("Encountered an empty attribute path");
172             }
173 
174             Path path = root;
175             // split the attribute based on a period for nested property paths, for example if you want to pass an attribute
176             // like "property1.property2" then JPA will not interpret that properly, you have to split in manually
177             String[] attrArray = attr.split("\\.");
178             for (String attrElement : attrArray) {
179                 if (StringUtils.isBlank(attrElement)) {
180                     throw new IllegalArgumentException("Encountered an empty path element in property path: " + attr);
181                 }
182                 path = path.get(attrElement);
183             }
184             return path;
185         }
186     }
187 
188     /**
189      * Creates a native JPA translator for queries.
190      *
191      * @param entityManager the entity manager to use for interacting with the database.
192      */
193     public NativeJpaQueryTranslator(EntityManager entityManager) {
194         this.entityManager = entityManager;
195     }
196 
197     /**
198      * {@inheritDoc}
199      */
200     @Override
201     public TypedQuery createQuery(Class queryClazz, TranslationContext criteria) {
202         CriteriaQuery jpaQuery = criteria.query;
203         // it is important to not create an empty or predicate
204         if (!criteria.predicates.isEmpty()) {
205             jpaQuery = jpaQuery.where(criteria.getCriteriaPredicate());
206         }
207         return entityManager.createQuery(jpaQuery);
208     }
209 
210     /**
211      * {@inheritDoc}
212      */
213     @Override
214     protected TranslationContext createCriteria(Class queryClazz) {
215         return new TranslationContext(entityManager, queryClazz);
216     }
217 
218     /**
219      * {@inheritDoc}
220      */
221     @Override
222     protected TranslationContext createInnerCriteria(TranslationContext parent) {
223         // just a container for the inner predicates
224         // copy everything else
225         return new TranslationContext(parent);
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
232     public void convertQueryFlags(QueryByCriteria qbc, TypedQuery query) {
233         final int startAtIndex = qbc.getStartAtIndex() != null ? qbc.getStartAtIndex() : 0;
234 
235         query.setFirstResult(startAtIndex);
236 
237         if (qbc.getMaxResults() != null) {
238             //not subtracting one from MaxResults in order to retrieve
239             //one extra row so that the MoreResultsAvailable field can be set
240             query.setMaxResults(qbc.getMaxResults());
241         }
242     }
243 
244     /**
245      * {@inheritDoc}
246      */
247     @Override
248     protected void addAnd(TranslationContext criteria, TranslationContext inner) {
249         criteria.and(inner);
250     }
251 
252     /**
253      * {@inheritDoc}
254      */
255     @Override
256     protected void addNotNull(TranslationContext criteria, String propertyPath) {
257         criteria.addPredicate(criteria.builder.isNotNull(criteria.attr(propertyPath)));
258     }
259 
260     /**
261      * {@inheritDoc}
262      */
263     @Override
264     protected void addIsNull(TranslationContext criteria, String propertyPath) {
265         criteria.addPredicate(criteria.builder.isNull(criteria.attr(propertyPath)));
266     }
267 
268     /**
269      * {@inheritDoc}
270      */
271     @Override
272     protected void addEqualTo(TranslationContext criteria, String propertyPath, Object value) {
273         criteria.addPredicate(criteria.builder.equal(criteria.attr(propertyPath), value));
274     }
275 
276     /**
277      * {@inheritDoc}
278      */
279     @Override
280     protected void addEqualToIgnoreCase(TranslationContext criteria, String propertyPath, String value) {
281         criteria.addPredicate(criteria.builder.equal(criteria.builder.upper(criteria.attr(propertyPath)), value.toUpperCase()));
282     }
283 
284     /**
285      * {@inheritDoc}
286      */
287     @Override
288     protected void addGreaterOrEqualTo(TranslationContext criteria, String propertyPath, Object value) {
289         criteria.addPredicate(criteria.builder.greaterThanOrEqualTo(criteria.attr(propertyPath), (Comparable) value));
290     }
291 
292     /**
293      * {@inheritDoc}
294      */
295     @Override
296     protected void addGreaterThan(TranslationContext criteria, String propertyPath, Object value) {
297         criteria.addPredicate(criteria.builder.greaterThan(criteria.attr(propertyPath), (Comparable) value));
298     }
299 
300     /**
301      * {@inheritDoc}
302      */
303     @Override
304     protected void addLessOrEqualTo(TranslationContext criteria, String propertyPath, Object value) {
305         criteria.addPredicate(criteria.builder.lessThanOrEqualTo(criteria.attr(propertyPath), (Comparable) value));
306     }
307 
308     /**
309      * {@inheritDoc}
310      */
311     @Override
312     protected void addLessThan(TranslationContext criteria, String propertyPath, Object value) {
313         criteria.addPredicate(criteria.builder.lessThan(criteria.attr(propertyPath), (Comparable) value));
314     }
315 
316     /**
317      * {@inheritDoc}
318      */
319     @Override
320     protected void addLike(TranslationContext criteria, String propertyPath, Object value) {
321         // value should be a String pattern
322 		criteria.addPredicate(criteria.builder.like(criteria.attr(propertyPath), fixSearchPattern(value.toString())));
323     }
324 
325     /**
326      * {@inheritDoc}
327      */
328     @Override
329     protected void addLikeIgnoreCase(TranslationContext criteria, String propertyPath, String value){
330         criteria.addPredicate(criteria.builder.like(criteria.builder.upper(criteria.attr(propertyPath)),
331                 fixSearchPattern(value.toUpperCase())));
332     }
333 
334 
335 	/**
336 	 * Fixes the search pattern by converting all non-escaped lookup wildcards ("*" and "?") into their respective JPQL
337 	 * wildcards ("%" and "_").
338      *
339      * <p>Any lookup wildcards escaped by a backslash are converted into their non-backslashed equivalents.</p>
340      *
341      * @param value the search pattern to fix.
342      * @return a fixed search pattern.
343 	 */
344 	protected String fixSearchPattern(String value) {
345 		StringBuilder fixedPattern = new StringBuilder(value);
346 		// Convert all non-escaped wildcards.
347 		for (int i = 0; i < LOOKUP_WILDCARDS.length; i++) {
348 			String lookupWildcard = LOOKUP_WILDCARDS[i];
349 			String escapedLookupWildcard = ESCAPED_LOOKUP_WILDCARDS[i];
350 			char jpqlWildcard = JPQL_WILDCARDS[i];
351 			int wildcardIndex = fixedPattern.indexOf(lookupWildcard);
352 			int escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard);
353 			while (wildcardIndex != -1) {
354 				if (wildcardIndex == 0 || escapedWildcardIndex != wildcardIndex - 1) {
355 					fixedPattern.setCharAt(wildcardIndex, jpqlWildcard);
356 					wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex);
357 				} else {
358 					fixedPattern.replace(escapedWildcardIndex, wildcardIndex + 1, lookupWildcard);
359 					wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex);
360 					escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard, wildcardIndex);
361 				}
362 			}
363 		}
364 		return fixedPattern.toString();
365 	}
366 
367     /**
368      * {@inheritDoc}
369      */
370     @Override
371     protected void addNotEqualTo(TranslationContext criteria, String propertyPath, Object value) {
372         criteria.addPredicate(criteria.builder.notEqual(criteria.attr(propertyPath), value));
373     }
374 
375     /**
376      * {@inheritDoc}
377      */
378     @Override
379     protected void addNotEqualToIgnoreCase(TranslationContext criteria, String propertyPath, String value) {
380         criteria.addPredicate(criteria.builder.notEqual(criteria.builder.upper(criteria.attr(propertyPath)), value.toUpperCase()));
381     }
382 
383     /**
384      * {@inheritDoc}
385      */
386     @Override
387     protected void addNotLike(TranslationContext criteria, String propertyPath, Object value) {
388         // value should be a String pattern
389 		criteria.addPredicate(criteria.builder.notLike(criteria.attr(propertyPath), fixSearchPattern(value.toString())));
390     }
391 
392     /**
393      * {@inheritDoc}
394      */
395     @Override
396     protected void addIn(TranslationContext criteria, String propertyPath, Collection values) {
397         criteria.addPredicate(criteria.attr(propertyPath).in(values));
398     }
399 
400     /**
401      * {@inheritDoc}
402      */
403     @Override
404     protected void addNotIn(TranslationContext criteria, String propertyPath, Collection values) {
405         criteria.addPredicate(criteria.builder.not(criteria.attr(propertyPath).in(values)));
406     }
407 
408     /**
409      * {@inheritDoc}
410      */
411     @Override
412     protected void addOr(TranslationContext criteria, TranslationContext inner) {
413         criteria.or(inner);
414     }
415 
416     /**
417      * {@inheritDoc}
418      */
419     @Override
420     protected String genUpperFunc(String pp) {
421         throw new IllegalStateException("genUpperFunc should not have been invoked for NativeJpaQueryTranslator");
422     }
423 
424 }