Java tutorial
/** * Copyright 2005-2014 The Kuali Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ecl2.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.kuali.rice.krad.data.jpa; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.Query; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaDelete; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Order; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.criteria.Subquery; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.kuali.rice.core.api.criteria.PropertyPath; import org.kuali.rice.core.api.criteria.QueryByCriteria; /** * JPA QueryTranslator that translates queries directly into native JPA 2 Criteria API. * * @author Kuali Rice Team (rice.collab@kuali.org) */ class NativeJpaQueryTranslator extends QueryTranslatorBase<NativeJpaQueryTranslator.TranslationContext, TypedQuery> { /** * Wildcard characters that are allowed in queries. */ protected static final String[] LOOKUP_WILDCARDS = { "*", "?" }; /** * Wildcard characters that are allowed in queries (in their escape formats). */ protected static final String[] ESCAPED_LOOKUP_WILDCARDS = { "\\*", "\\?" }; /** * Wildcard character equivalents in JPQL. */ protected static final char[] JPQL_WILDCARDS = { '%', '_' }; /** * The entity manager for interacting with the database. */ protected EntityManager entityManager; /** * Thin abstraction/container for criteria parsing context. */ public static class TranslationContext { /** * The builder for the criteria. */ CriteriaBuilder builder; /** * The query for the criteria. */ CriteriaQuery query; /** * The FROM clause root type. */ Root root; /** * The list of predicates. */ List<Predicate> predicates = new ArrayList<Predicate>(); /** * If the current context is a sub-query of another context, the parent context will be stored here. */ TranslationContext parentTranslationContext; /** * Creates a new criteria parsing context. * * @param entityManager the entity manager to use for interacting with the database. * @param queryClass the type of the query. */ TranslationContext(EntityManager entityManager, Class queryClass) { builder = entityManager.getCriteriaBuilder(); query = builder.createQuery(queryClass); // establish single consistent root instance // we need this or erroneous query against distinct table aliases is generated root = query.from(query.getResultType()); } /** * * Creates a new criteria parsing context for an inner subquery. The parent context is stored * to allow references between the inner and outer queries. * * @param entityManager the entity manager to use for interacting with the database. * @param queryClass the type of the query. * @param parentContext The {@link TranslationContext} of the outer query into which the subquery will be added as a {@link Predicate}. */ TranslationContext(EntityManager entityManager, Class queryClass, TranslationContext parentContext) { this(entityManager, queryClass); this.parentTranslationContext = parentContext; } /** * Creates a new criteria parsing context that is a container for the inner predicates. * * @param parent the parent criteria parsing context. */ TranslationContext(TranslationContext parent) { builder = parent.builder; query = parent.query; root = parent.root; parentTranslationContext = parent.parentTranslationContext; } /** * Adds a predicate. * * @param predicate the predicate to add. */ void addPredicate(Predicate predicate) { predicates.add(predicate); } /** * Adds an AND clause. * * @param predicate the predicate to AND. */ void and(TranslationContext predicate) { addPredicate(predicate.getCriteriaPredicate()); } /** * Adds a JPA Subquery to the predicates. * * @param subquery the subquery to add. */ void addExistsSubquery(Subquery<?> subquery) { predicates.add(builder.exists(subquery)); } /** * Adds an OR clause. * * @param predicate the predicate to OR. */ void or(TranslationContext predicate) { List<Predicate> newpredicates = new ArrayList<Predicate>(); //When traversed to a simple OR predicate you may not have a criteria predicate set so check and just //add to builder if necKradEclipseLinkEntityManagerFactoryessary Predicate criteriaPredicate = getCriteriaPredicate(); Predicate orPredicate = null; if (criteriaPredicate != null) { orPredicate = builder .or(new Predicate[] { predicate.getCriteriaPredicate(), getCriteriaPredicate() }); } else { orPredicate = builder.or(predicate.getCriteriaPredicate()); } newpredicates.add(orPredicate); predicates = newpredicates; } /** * Gets the criteria predicate. * * @return the criteria predicate. */ Predicate getCriteriaPredicate() { if (predicates.size() == 1) { return predicates.get(0); } else if (predicates.size() > 1) { return builder.and(predicates.toArray(new Predicate[predicates.size()])); } else { return null; } } /** * Gets the path for the given attribute. * * @param attr the attribute path. * @return the path for the given attribute. */ @SuppressWarnings("rawtypes") Path attr(String attr) { if (StringUtils.isBlank(attr)) { throw new IllegalArgumentException("Encountered an empty attribute path"); } // Tokenize the property string String[] attrArray = attr.split("\\."); // first, check if this is a reference to a field on the parent (outer) query. // If so, and we have a parent (outer) query, then strip off the parent keyword // and resolve the property in that context. if (attrArray.length > 0 && StringUtils.equals(attrArray[0], "parent") && parentTranslationContext != null) { return parentTranslationContext.attr(StringUtils.substringAfter(attr, ".")); } else { Path path = root; // split the attribute based on a period for nested property paths, for example if you want to pass an // attribute // like "property1.property2" then JPA will not interpret that properly, you have to split in manually for (String attrElement : attrArray) { if (StringUtils.isBlank(attrElement)) { throw new IllegalArgumentException( "Encountered an empty path element in property path: " + attr); } path = path.get(attrElement); } return path; } } } /** * Creates a native JPA translator for queries. * * @param entityManager the entity manager to use for interacting with the database. */ public NativeJpaQueryTranslator(EntityManager entityManager) { this.entityManager = entityManager; } /** * {@inheritDoc} */ @Override public TypedQuery createQuery(Class queryClazz, TranslationContext criteria) { CriteriaQuery jpaQuery = criteria.query; // it is important to not create an empty or predicate if (!criteria.predicates.isEmpty()) { jpaQuery = jpaQuery.where(criteria.getCriteriaPredicate()); } return entityManager.createQuery(jpaQuery); } /** * {@inheritDoc} */ public Query createDeletionQuery(Class queryClazz, TranslationContext criteria) { CriteriaDelete jpaQuery = entityManager.getCriteriaBuilder().createCriteriaDelete(queryClazz); if (!criteria.predicates.isEmpty()) { jpaQuery = jpaQuery.where(criteria.getCriteriaPredicate()); } return entityManager.createQuery(jpaQuery); } /** * {@inheritDoc} */ @Override protected TranslationContext createCriteria(Class queryClazz) { return new TranslationContext(entityManager, queryClazz); } /** * {@inheritDoc} */ @Override protected TranslationContext createCriteriaForSubQuery(Class queryClazz, TranslationContext parentContext) { return new TranslationContext(entityManager, queryClazz, parentContext); } /** * {@inheritDoc} */ @Override protected TranslationContext createInnerCriteria(TranslationContext parent) { // just a container for the inner predicates // copy everything else return new TranslationContext(parent); } /** * {@inheritDoc} */ @Override public void convertQueryFlags(QueryByCriteria qbc, TypedQuery query) { final int startAtIndex = qbc.getStartAtIndex() != null ? qbc.getStartAtIndex() : 0; query.setFirstResult(startAtIndex); if (qbc.getMaxResults() != null) { //not subtracting one from MaxResults in order to retrieve //one extra row so that the MoreResultsAvailable field can be set query.setMaxResults(qbc.getMaxResults()); } } /** * {@inheritDoc} */ @Override protected void addAnd(TranslationContext criteria, TranslationContext inner) { criteria.and(inner); } /** * {@inheritDoc} */ @Override protected void addNotNull(TranslationContext criteria, String propertyPath) { criteria.addPredicate(criteria.builder.isNotNull(criteria.attr(propertyPath))); } /** * {@inheritDoc} */ @Override protected void addIsNull(TranslationContext criteria, String propertyPath) { criteria.addPredicate(criteria.builder.isNull(criteria.attr(propertyPath))); } /** * Translates the Rice Criteria API {@link PropertyPath} object into a native JPA path which can be used in JPA * predicates. * * @param criteria * The base criteria context for translation of the property if no specific data type is given. * @param value * The {@link PropertyPath} object passed in from the Rice Criteria API. * @return A JPA {@link Path} object which can be used in JPA {@link Predicate} statements. */ @SuppressWarnings("rawtypes") protected Path translatePropertyPathIntoJpaPath(TranslationContext criteria, PropertyPath value) { TranslationContext tempCriteria = criteria; if (value.getDataType() != null) { try { tempCriteria = createCriteria(Class.forName(value.getDataType())); } catch (ClassNotFoundException e) { // unable to find the type - ignore and attempt to resolve path without special context Logger.getLogger(this.getClass()).error("Unable to find data type " + value.getDataType() + ". Falling back to the base root for the query: " + criteria.root.getJavaType()); } } return tempCriteria.attr(value.getPropertyPath()); } /** * {@inheritDoc} */ @Override protected void addEqualTo(TranslationContext criteria, String propertyPath, Object value) { // If this is a property path criteria, we need to translate it first if (value instanceof PropertyPath) { // We *must* make the call separate here. If we don't, it binds to the (Expression,Object) version of the // JPA method // which converts our property path into a string literal. Path path = translatePropertyPathIntoJpaPath(criteria, (PropertyPath) value); criteria.addPredicate(criteria.builder.equal(criteria.attr(propertyPath), path)); } else { criteria.addPredicate(criteria.builder.equal(criteria.attr(propertyPath), value)); } } /** * {@inheritDoc} */ @Override protected void addEqualToIgnoreCase(TranslationContext criteria, String propertyPath, String value) { criteria.addPredicate( criteria.builder.equal(criteria.builder.upper(criteria.attr(propertyPath)), value.toUpperCase())); } /** * {@inheritDoc} */ @Override protected void addGreaterOrEqualTo(TranslationContext criteria, String propertyPath, Object value) { criteria.addPredicate( criteria.builder.greaterThanOrEqualTo(criteria.attr(propertyPath), (Comparable) value)); } /** * {@inheritDoc} */ @Override protected void addGreaterThan(TranslationContext criteria, String propertyPath, Object value) { criteria.addPredicate(criteria.builder.greaterThan(criteria.attr(propertyPath), (Comparable) value)); } /** * {@inheritDoc} */ @Override protected void addLessOrEqualTo(TranslationContext criteria, String propertyPath, Object value) { criteria.addPredicate(criteria.builder.lessThanOrEqualTo(criteria.attr(propertyPath), (Comparable) value)); } /** * {@inheritDoc} */ @Override protected void addLessThan(TranslationContext criteria, String propertyPath, Object value) { criteria.addPredicate(criteria.builder.lessThan(criteria.attr(propertyPath), (Comparable) value)); } /** * {@inheritDoc} */ @Override protected void addLike(TranslationContext criteria, String propertyPath, Object value) { // value should be a String pattern criteria.addPredicate( criteria.builder.like(criteria.attr(propertyPath), fixSearchPattern(value.toString()))); } /** * {@inheritDoc} */ @Override protected void addLikeIgnoreCase(TranslationContext criteria, String propertyPath, String value) { criteria.addPredicate(criteria.builder.like(criteria.builder.upper(criteria.attr(propertyPath)), fixSearchPattern(value.toUpperCase()))); } /** * {@inheritDoc} */ @Override protected void addExistsSubquery(TranslationContext criteria, String subQueryType, org.kuali.rice.core.api.criteria.Predicate subQueryPredicate) { try { Class<?> subQueryBaseClass = Class.forName(subQueryType); Subquery<?> subquery = criteria.query.subquery(subQueryBaseClass); TranslationContext subQueryJpaPredicate = createCriteriaForSubQuery(subQueryBaseClass, criteria); // If a subQueryPredicate is passed, this is a Rice Predicate object and must be translated // into JPA - so we add it to the list this way. if (subQueryPredicate != null) { addPredicate(subQueryPredicate, subQueryJpaPredicate); } subquery.where(subQueryJpaPredicate.predicates.toArray(new Predicate[0])); criteria.addExistsSubquery(subquery); } catch (ClassNotFoundException e) { throw new IllegalArgumentException(subQueryType + " can not be resolved to a class for JPA"); } } /** * Fixes the search pattern by converting all non-escaped lookup wildcards ("*" and "?") into their respective JPQL * wildcards ("%" and "_"). * * <p>Any lookup wildcards escaped by a backslash are converted into their non-backslashed equivalents.</p> * * @param value the search pattern to fix. * @return a fixed search pattern. */ protected String fixSearchPattern(String value) { StringBuilder fixedPattern = new StringBuilder(value); // Convert all non-escaped wildcards. for (int i = 0; i < LOOKUP_WILDCARDS.length; i++) { String lookupWildcard = LOOKUP_WILDCARDS[i]; String escapedLookupWildcard = ESCAPED_LOOKUP_WILDCARDS[i]; char jpqlWildcard = JPQL_WILDCARDS[i]; int wildcardIndex = fixedPattern.indexOf(lookupWildcard); int escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard); while (wildcardIndex != -1) { if (wildcardIndex == 0 || escapedWildcardIndex != wildcardIndex - 1) { fixedPattern.setCharAt(wildcardIndex, jpqlWildcard); wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex); } else { fixedPattern.replace(escapedWildcardIndex, wildcardIndex + 1, lookupWildcard); wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex); escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard, wildcardIndex); } } } return fixedPattern.toString(); } /** * {@inheritDoc} */ @Override protected void addNotEqualTo(TranslationContext criteria, String propertyPath, Object value) { // If this is a property path criteria, we need to translate it first if (value instanceof PropertyPath) { // We *must* make the call separate here. If we don't, it binds to the (Expression,Object) version of the // JPA method // which converts our property path into a string literal. Path path = translatePropertyPathIntoJpaPath(criteria, (PropertyPath) value); criteria.addPredicate(criteria.builder.notEqual(criteria.attr(propertyPath), path)); } else { criteria.addPredicate(criteria.builder.notEqual(criteria.attr(propertyPath), value)); } } /** * {@inheritDoc} */ @Override protected void addNotEqualToIgnoreCase(TranslationContext criteria, String propertyPath, String value) { criteria.addPredicate(criteria.builder.notEqual(criteria.builder.upper(criteria.attr(propertyPath)), value.toUpperCase())); } /** * {@inheritDoc} */ @Override protected void addNotLike(TranslationContext criteria, String propertyPath, Object value) { // value should be a String pattern criteria.addPredicate( criteria.builder.notLike(criteria.attr(propertyPath), fixSearchPattern(value.toString()))); } /** * {@inheritDoc} */ @Override protected void addIn(TranslationContext criteria, String propertyPath, Collection values) { criteria.addPredicate(criteria.attr(propertyPath).in(values)); } /** * {@inheritDoc} */ @Override protected void addNotIn(TranslationContext criteria, String propertyPath, Collection values) { criteria.addPredicate(criteria.builder.not(criteria.attr(propertyPath).in(values))); } /** * {@inheritDoc} */ @Override protected void addOr(TranslationContext criteria, TranslationContext inner) { criteria.or(inner); } /** * {@inheritDoc} */ @Override protected void addOrderBy(TranslationContext criteria, String propertyPath, boolean sortAscending) { List<Order> orderList = criteria.query.getOrderList(); if (orderList == null) { orderList = new ArrayList<Order>(); } if (sortAscending) { orderList.add(criteria.builder.asc(criteria.root.get(propertyPath))); } else { orderList.add(criteria.builder.desc(criteria.root.get(propertyPath))); } criteria.query.orderBy(orderList); } // protected void addSubquery( TranslationContext criteria ) /** * {@inheritDoc} */ @Override protected String genUpperFunc(String pp) { throw new IllegalStateException("genUpperFunc should not have been invoked for NativeJpaQueryTranslator"); } }