View Javadoc
1   /**
2    * Copyright 2005-2016 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.Query;
24  import javax.persistence.TypedQuery;
25  import javax.persistence.criteria.CriteriaBuilder;
26  import javax.persistence.criteria.CriteriaDelete;
27  import javax.persistence.criteria.CriteriaQuery;
28  import javax.persistence.criteria.Order;
29  import javax.persistence.criteria.Path;
30  import javax.persistence.criteria.Predicate;
31  import javax.persistence.criteria.Root;
32  import javax.persistence.criteria.Subquery;
33  
34  import org.apache.commons.lang.StringUtils;
35  import org.apache.log4j.Logger;
36  import org.kuali.rice.core.api.criteria.PropertyPath;
37  import org.kuali.rice.core.api.criteria.QueryByCriteria;
38  
39  /**
40   * JPA QueryTranslator that translates queries directly into native JPA 2 Criteria API.
41   *
42   * @author Kuali Rice Team (rice.collab@kuali.org)
43   */
44  class NativeJpaQueryTranslator extends QueryTranslatorBase<NativeJpaQueryTranslator.TranslationContext, TypedQuery> {
45  
46      /**
47       * Wildcard characters that are allowed in queries.
48       */
49  	protected static final String[] LOOKUP_WILDCARDS = { "*", "?" };
50  
51      /**
52       * Wildcard characters that are allowed in queries (in their escape formats).
53       */
54  	protected static final String[] ESCAPED_LOOKUP_WILDCARDS = { "\\*", "\\?" };
55  
56      /**
57       * Wildcard character equivalents in JPQL.
58       */
59  	protected static final char[] JPQL_WILDCARDS = { '%', '_' };
60  
61      /**
62       * The entity manager for interacting with the database.
63       */
64      protected EntityManager entityManager;
65  
66      /**
67       * Thin abstraction/container for criteria parsing context.
68       */
69      public static class TranslationContext {
70  
71          /**
72           * The builder for the criteria.
73           */
74          CriteriaBuilder builder;
75  
76          /**
77           * The query for the criteria.
78           */
79          CriteriaQuery query;
80  
81          /**
82           * The FROM clause root type.
83           */
84          Root root;
85  
86          /**
87           * The list of predicates.
88           */
89          List<Predicate> predicates = new ArrayList<Predicate>();
90  
91  		/**
92  		 * If the current context is a sub-query of another context, the parent context will be stored here.
93  		 */
94  		TranslationContext parentTranslationContext;
95  
96          /**
97           * Creates a new criteria parsing context.
98           *
99           * @param entityManager the entity manager to use for interacting with the database.
100          * @param queryClass the type of the query.
101          */
102         TranslationContext(EntityManager entityManager, Class queryClass) {
103             builder = entityManager.getCriteriaBuilder();
104             query = builder.createQuery(queryClass);
105             // establish single consistent root instance
106             // we need this or erroneous query against distinct table aliases is generated
107             root = query.from(query.getResultType());
108         }
109 
110         /**
111          * 
112          * Creates a new criteria parsing context for an inner subquery.  The parent context is stored
113          * to allow references between the inner and outer queries.
114          * 
115          * @param entityManager the entity manager to use for interacting with the database.
116          * @param queryClass the type of the query.
117          * @param parentContext The {@link TranslationContext} of the outer query into which the subquery will be added as a {@link Predicate}.
118          */
119         TranslationContext( EntityManager entityManager, Class queryClass, TranslationContext parentContext ) {
120         	this(entityManager, queryClass);
121         	this.parentTranslationContext = parentContext;
122         }
123         
124         /**
125          * Creates a new criteria parsing context that is a container for the inner predicates.
126          *
127          * @param parent the parent criteria parsing context.
128          */
129         TranslationContext(TranslationContext parent) {
130             builder = parent.builder;
131             query = parent.query;
132             root = parent.root;
133 			parentTranslationContext = parent.parentTranslationContext;
134         }
135 
136         /**
137          * Adds a predicate.
138          *
139          * @param predicate the predicate to add.
140          */
141         void addPredicate(Predicate predicate) {
142             predicates.add(predicate);
143         }
144 
145         /**
146          * Adds an AND clause.
147          *
148          * @param predicate the predicate to AND.
149          */
150         void and(TranslationContext predicate) {
151             addPredicate(predicate.getCriteriaPredicate());
152         }
153 
154 		/**
155 		 * Adds a JPA Subquery to the predicates.
156 		 * 
157 		 * @param subquery the subquery to add.
158 		 */
159 		void addExistsSubquery(Subquery<?> subquery) {
160 			predicates.add(builder.exists(subquery));
161 		}
162 
163         /**
164          * Adds an OR clause.
165          *
166          * @param predicate the predicate to OR.
167          */
168         void or(TranslationContext predicate) {
169             List<Predicate> newpredicates = new ArrayList<Predicate>();
170             //When traversed to a simple OR predicate you may not have a criteria predicate set so check and just
171             //add to builder if necKradEclipseLinkEntityManagerFactoryessary
172             Predicate criteriaPredicate = getCriteriaPredicate();
173             Predicate orPredicate = null;
174             if(criteriaPredicate != null){
175                 orPredicate = builder.or(new Predicate[] {  predicate.getCriteriaPredicate(), getCriteriaPredicate() });
176             } else {
177                 orPredicate = builder.or(predicate.getCriteriaPredicate());
178             }
179             newpredicates.add(orPredicate);
180             predicates = newpredicates;
181         }
182 
183         /**
184          * Gets the criteria predicate.
185          *
186          * @return the criteria predicate.
187          */
188         Predicate getCriteriaPredicate() {
189             if (predicates.size() == 1) {
190                 return predicates.get(0);
191             } else if(predicates.size() > 1){
192                 return builder.and(predicates.toArray(new Predicate[predicates.size()]));
193             } else {
194                 return null;
195             }
196         }
197 
198         /**
199          * Gets the path for the given attribute.
200          *
201          * @param attr the attribute path.
202          * @return the path for the given attribute.
203          */
204 		@SuppressWarnings("rawtypes")
205 		Path attr(String attr) {
206             if (StringUtils.isBlank(attr)) {
207                 throw new IllegalArgumentException("Encountered an empty attribute path");
208             }
209 
210 			// Tokenize the property string
211             String[] attrArray = attr.split("\\.");
212 			// first, check if this is a reference to a field on the parent (outer) query.
213 			// If so, and we have a parent (outer) query, then strip off the parent keyword
214 			// and resolve the property in that context.
215 			if (attrArray.length > 0 && StringUtils.equals(attrArray[0], "parent") && parentTranslationContext != null) {
216 				return parentTranslationContext.attr(StringUtils.substringAfter(attr, "."));
217 			} else {
218 				Path path = root;
219 				// split the attribute based on a period for nested property paths, for example if you want to pass an
220 				// attribute
221 				// like "property1.property2" then JPA will not interpret that properly, you have to split in manually
222 				for (String attrElement : attrArray) {
223 					if (StringUtils.isBlank(attrElement)) {
224 						throw new IllegalArgumentException("Encountered an empty path element in property path: "
225 								+ attr);
226 					}
227 					path = path.get(attrElement);
228 				}
229 				return path;
230             }
231         }
232     }
233 
234     /**
235      * Creates a native JPA translator for queries.
236      *
237      * @param entityManager the entity manager to use for interacting with the database.
238      */
239     public NativeJpaQueryTranslator(EntityManager entityManager) {
240         this.entityManager = entityManager;
241     }
242 
243     /**
244      * {@inheritDoc}
245      */
246     @Override
247     public TypedQuery createQuery(Class queryClazz, TranslationContext criteria) {
248         CriteriaQuery jpaQuery = criteria.query;
249         // it is important to not create an empty or predicate
250         if (!criteria.predicates.isEmpty()) {
251             jpaQuery = jpaQuery.where(criteria.getCriteriaPredicate());
252         }
253         return entityManager.createQuery(jpaQuery);
254     }
255 
256     /**
257      * {@inheritDoc}
258      */
259     @Override
260 	public Query createDeletionQuery(Class queryClazz, TranslationContext criteria) {
261         CriteriaDelete jpaQuery = entityManager.getCriteriaBuilder().createCriteriaDelete(queryClazz);
262 
263         if (!criteria.predicates.isEmpty()) {
264             jpaQuery = jpaQuery.where(criteria.getCriteriaPredicate());
265         }
266 
267         return entityManager.createQuery(jpaQuery);
268     }
269 
270     /**
271      * {@inheritDoc}
272      */
273     @Override
274     protected TranslationContext createCriteria(Class queryClazz) {
275         return new TranslationContext(entityManager, queryClazz);
276     }
277 
278     /**
279      * {@inheritDoc}
280      */
281     @Override
282 	protected TranslationContext createCriteriaForSubQuery(Class queryClazz, TranslationContext parentContext) {
283 		return new TranslationContext(entityManager, queryClazz, parentContext);
284 	}
285 
286 	/**
287 	 * {@inheritDoc}
288 	 */
289 	@Override
290     protected TranslationContext createInnerCriteria(TranslationContext parent) {
291         // just a container for the inner predicates
292         // copy everything else
293         return new TranslationContext(parent);
294     }
295 
296     /**
297      * {@inheritDoc}
298      */
299     @Override
300     public void convertQueryFlags(QueryByCriteria qbc, TypedQuery query) {
301         final int startAtIndex = qbc.getStartAtIndex() != null ? qbc.getStartAtIndex() : 0;
302 
303         query.setFirstResult(startAtIndex);
304 
305         if (qbc.getMaxResults() != null) {
306             //not subtracting one from MaxResults in order to retrieve
307             //one extra row so that the MoreResultsAvailable field can be set
308             query.setMaxResults(qbc.getMaxResults());
309         }
310     }
311 
312     /**
313      * {@inheritDoc}
314      */
315     @Override
316     protected void addAnd(TranslationContext criteria, TranslationContext inner) {
317         criteria.and(inner);
318     }
319 
320     /**
321      * {@inheritDoc}
322      */
323     @Override
324     protected void addNotNull(TranslationContext criteria, String propertyPath) {
325         criteria.addPredicate(criteria.builder.isNotNull(criteria.attr(propertyPath)));
326     }
327 
328     /**
329      * {@inheritDoc}
330      */
331     @Override
332     protected void addIsNull(TranslationContext criteria, String propertyPath) {
333         criteria.addPredicate(criteria.builder.isNull(criteria.attr(propertyPath)));
334     }
335 
336 	/**
337 	 * Translates the Rice Criteria API {@link PropertyPath} object into a native JPA path which can be used in JPA
338 	 * predicates.
339 	 * 
340 	 * @param criteria
341 	 *            The base criteria context for translation of the property if no specific data type is given.
342 	 * @param value
343 	 *            The {@link PropertyPath} object passed in from the Rice Criteria API.
344 	 * @return A JPA {@link Path} object which can be used in JPA {@link Predicate} statements.
345 	 */
346 	@SuppressWarnings("rawtypes")
347 	protected Path translatePropertyPathIntoJpaPath(TranslationContext criteria, PropertyPath value) {
348 		TranslationContext tempCriteria = criteria;
349 		if (value.getDataType() != null) {
350 			try {
351 				tempCriteria = createCriteria(Class.forName(value.getDataType()));
352 			} catch (ClassNotFoundException e) {
353 				// unable to find the type - ignore and attempt to resolve path without special context
354 				Logger.getLogger(this.getClass()).error(
355 						"Unable to find data type " + value.getDataType()
356 								+ ".  Falling back to the base root for the query: " + criteria.root.getJavaType());
357 			}
358 		}
359 		return tempCriteria.attr(value.getPropertyPath());
360 	}
361 
362     /**
363      * {@inheritDoc}
364      */
365     @Override
366     protected void addEqualTo(TranslationContext criteria, String propertyPath, Object value) {
367 		// If this is a property path criteria, we need to translate it first
368 		if (value instanceof PropertyPath) {
369 			// We *must* make the call separate here. If we don't, it binds to the (Expression,Object) version of the
370 			// JPA method
371 			// which converts our property path into a string literal.
372 			Path path = translatePropertyPathIntoJpaPath(criteria, (PropertyPath) value);
373 			criteria.addPredicate(criteria.builder.equal(criteria.attr(propertyPath), path));
374 		} else {
375 			criteria.addPredicate(criteria.builder.equal(criteria.attr(propertyPath), value));
376 		}
377     }
378 
379     /**
380      * {@inheritDoc}
381      */
382     @Override
383     protected void addEqualToIgnoreCase(TranslationContext criteria, String propertyPath, String value) {
384         criteria.addPredicate(criteria.builder.equal(criteria.builder.upper(criteria.attr(propertyPath)), value.toUpperCase()));
385     }
386 
387     /**
388      * {@inheritDoc}
389      */
390     @Override
391     protected void addGreaterOrEqualTo(TranslationContext criteria, String propertyPath, Object value) {
392         criteria.addPredicate(criteria.builder.greaterThanOrEqualTo(criteria.attr(propertyPath), (Comparable) value));
393     }
394 
395     /**
396      * {@inheritDoc}
397      */
398     @Override
399     protected void addGreaterThan(TranslationContext criteria, String propertyPath, Object value) {
400         criteria.addPredicate(criteria.builder.greaterThan(criteria.attr(propertyPath), (Comparable) value));
401     }
402 
403     /**
404      * {@inheritDoc}
405      */
406     @Override
407     protected void addLessOrEqualTo(TranslationContext criteria, String propertyPath, Object value) {
408         criteria.addPredicate(criteria.builder.lessThanOrEqualTo(criteria.attr(propertyPath), (Comparable) value));
409     }
410 
411     /**
412      * {@inheritDoc}
413      */
414     @Override
415     protected void addLessThan(TranslationContext criteria, String propertyPath, Object value) {
416         criteria.addPredicate(criteria.builder.lessThan(criteria.attr(propertyPath), (Comparable) value));
417     }
418 
419     /**
420      * {@inheritDoc}
421      */
422     @Override
423     protected void addLike(TranslationContext criteria, String propertyPath, Object value) {
424         // value should be a String pattern
425 		criteria.addPredicate(criteria.builder.like(criteria.attr(propertyPath), fixSearchPattern(value.toString())));
426     }
427 
428     /**
429      * {@inheritDoc}
430      */
431     @Override
432     protected void addLikeIgnoreCase(TranslationContext criteria, String propertyPath, String value){
433         criteria.addPredicate(criteria.builder.like(criteria.builder.upper(criteria.attr(propertyPath)),
434                 fixSearchPattern(value.toUpperCase())));
435     }
436 
437 	/**
438 	 * {@inheritDoc}
439 	 */
440 	@Override
441 	protected void addNotLikeIgnoreCase(TranslationContext criteria, String propertyPath, String value) {
442 		criteria.addPredicate(criteria.builder.notLike(criteria.builder.upper(criteria.attr(propertyPath)),
443 				fixSearchPattern(value.toUpperCase())));
444 	}
445 
446 	/**
447 	 * {@inheritDoc}
448 	 */
449 	@Override
450 	protected void addExistsSubquery(TranslationContext criteria, String subQueryType,
451 			org.kuali.rice.core.api.criteria.Predicate subQueryPredicate) {
452 		try {
453 			Class<?> subQueryBaseClass = Class.forName(subQueryType);
454 			Subquery<?> subquery = criteria.query.subquery(subQueryBaseClass);
455 			TranslationContext subQueryJpaPredicate = createCriteriaForSubQuery(subQueryBaseClass, criteria);
456 
457 			// If a subQueryPredicate is passed, this is a Rice Predicate object and must be translated
458 			// into JPA - so we add it to the list this way.
459 			if (subQueryPredicate != null) {
460 				addPredicate(subQueryPredicate, subQueryJpaPredicate);
461 			}
462 
463 			subquery.where(subQueryJpaPredicate.predicates.toArray(new Predicate[0]));
464 			criteria.addExistsSubquery(subquery);
465 		} catch (ClassNotFoundException e) {
466 			throw new IllegalArgumentException(subQueryType + " can not be resolved to a class for JPA");
467 		}
468 	}
469 
470 	/**
471 	 * Fixes the search pattern by converting all non-escaped lookup wildcards ("*" and "?") into their respective JPQL
472 	 * wildcards ("%" and "_").
473      *
474      * <p>Any lookup wildcards escaped by a backslash are converted into their non-backslashed equivalents.</p>
475      *
476      * @param value the search pattern to fix.
477      * @return a fixed search pattern.
478 	 */
479 	protected String fixSearchPattern(String value) {
480 		StringBuilder fixedPattern = new StringBuilder(value);
481 		// Convert all non-escaped wildcards.
482 		for (int i = 0; i < LOOKUP_WILDCARDS.length; i++) {
483 			String lookupWildcard = LOOKUP_WILDCARDS[i];
484 			String escapedLookupWildcard = ESCAPED_LOOKUP_WILDCARDS[i];
485 			char jpqlWildcard = JPQL_WILDCARDS[i];
486 			int wildcardIndex = fixedPattern.indexOf(lookupWildcard);
487 			int escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard);
488 			while (wildcardIndex != -1) {
489 				if (wildcardIndex == 0 || escapedWildcardIndex != wildcardIndex - 1) {
490 					fixedPattern.setCharAt(wildcardIndex, jpqlWildcard);
491 					wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex);
492 				} else {
493 					fixedPattern.replace(escapedWildcardIndex, wildcardIndex + 1, lookupWildcard);
494 					wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex);
495 					escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard, wildcardIndex);
496 				}
497 			}
498 		}
499 		return fixedPattern.toString();
500 	}
501 
502     /**
503      * {@inheritDoc}
504      */
505     @Override
506     protected void addNotEqualTo(TranslationContext criteria, String propertyPath, Object value) {
507 		// If this is a property path criteria, we need to translate it first
508 		if (value instanceof PropertyPath) {
509 			// We *must* make the call separate here. If we don't, it binds to the (Expression,Object) version of the
510 			// JPA method
511 			// which converts our property path into a string literal.
512 			Path path = translatePropertyPathIntoJpaPath(criteria, (PropertyPath) value);
513 			criteria.addPredicate(criteria.builder.notEqual(criteria.attr(propertyPath), path));
514 		} else {
515 			criteria.addPredicate(criteria.builder.notEqual(criteria.attr(propertyPath), value));
516 		}
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
523     protected void addNotEqualToIgnoreCase(TranslationContext criteria, String propertyPath, String value) {
524         criteria.addPredicate(criteria.builder.notEqual(criteria.builder.upper(criteria.attr(propertyPath)), value.toUpperCase()));
525     }
526 
527     /**
528      * {@inheritDoc}
529      */
530     @Override
531     protected void addNotLike(TranslationContext criteria, String propertyPath, Object value) {
532         // value should be a String pattern
533 		criteria.addPredicate(criteria.builder.notLike(criteria.attr(propertyPath), fixSearchPattern(value.toString())));
534     }
535 
536     /**
537      * {@inheritDoc}
538      */
539     @Override
540     protected void addIn(TranslationContext criteria, String propertyPath, Collection values) {
541         criteria.addPredicate(criteria.attr(propertyPath).in(values));
542     }
543 
544     /**
545      * {@inheritDoc}
546      */
547     @Override
548     protected void addNotIn(TranslationContext criteria, String propertyPath, Collection values) {
549         criteria.addPredicate(criteria.builder.not(criteria.attr(propertyPath).in(values)));
550     }
551 
552     /**
553      * {@inheritDoc}
554      */
555     @Override
556     protected void addOr(TranslationContext criteria, TranslationContext inner) {
557         criteria.or(inner);
558     }
559 
560     /**
561      * {@inheritDoc}
562      */
563     @Override
564     protected void addOrderBy(TranslationContext criteria, String propertyPath, boolean sortAscending) {
565         List<Order> orderList = criteria.query.getOrderList();
566         if (orderList == null) {
567             orderList = new ArrayList<Order>();
568         }
569 
570         if (propertyPath.contains(".")) {
571             String propertyPathStart = StringUtils.substringBefore( propertyPath, "." );
572             String propertyPathEnd = StringUtils.substringAfter( propertyPath, "." );
573 
574             if (sortAscending) {
575                 orderList.add(criteria.builder.asc(criteria.root.get(propertyPathStart).get(propertyPathEnd)));
576             } else {
577                 orderList.add(criteria.builder.desc(criteria.root.get(propertyPathStart).get(propertyPathEnd)));
578             }
579         } else {
580             if (sortAscending) {
581                 orderList.add(criteria.builder.asc(criteria.root.get(propertyPath)));
582             } else {
583                 orderList.add(criteria.builder.desc(criteria.root.get(propertyPath)));
584             }
585         }
586 
587         criteria.query.orderBy(orderList);
588     }
589 
590 	// protected void addSubquery( TranslationContext criteria )
591 
592     /**
593      * {@inheritDoc}
594      */
595     @Override
596     protected String genUpperFunc(String pp) {
597         throw new IllegalStateException("genUpperFunc should not have been invoked for NativeJpaQueryTranslator");
598     }
599 
600 }