/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you under the Apache 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.jpa; import com.google.common.base.Function; import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.ParameterExpression; import javax.persistence.criteria.Root; import javax.persistence.metamodel.Attribute; import org.hibernate.LockOptions; import org.hibernate.NaturalIdLoadAccess; import org.hibernate.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionOperations; /** * Base class for JPA DAOs in the portal that contains common functions. * */ public abstract class BaseJpaDao implements InitializingBean, ApplicationContextAware { private static final String QUERY_SUFFIX = ".Query"; protected final Logger logger = LoggerFactory.getLogger(getClass()); private ApplicationContext applicationContext; protected abstract EntityManager getEntityManager(); protected abstract TransactionOperations getTransactionOperations(); public ApplicationContext getApplicationContext() { return applicationContext; } @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public void afterPropertiesSet() throws Exception {} protected final <T> ParameterExpression<T> createParameterExpression( Class<T> paramClass, String name) { final EntityManager entityManager = this.getEntityManager(); final EntityManagerFactory entityManagerFactory = entityManager.getEntityManagerFactory(); final CriteriaBuilder criteriaBuilder = entityManagerFactory.getCriteriaBuilder(); return criteriaBuilder.parameter(paramClass, name); } /** * Factory method for creating a {@link CriteriaQuery} employing standards and best practices in * general use within the portal. Query objects returned from this method should normally be * passed to {@link createCachedQuery}; this step is important for the sake of scalability. */ protected final <T> CriteriaQuery<T> createCriteriaQuery( Function<CriteriaBuilder, CriteriaQuery<T>> builder) { final EntityManager entityManager = this.getEntityManager(); final EntityManagerFactory entityManagerFactory = entityManager.getEntityManagerFactory(); final CriteriaBuilder criteriaBuilder = entityManagerFactory.getCriteriaBuilder(); final CriteriaQuery<T> criteriaQuery = builder.apply(criteriaBuilder); //Do in TX so the EM gets closed correctly final TransactionOperations transactionOperations = this.getTransactionOperations(); transactionOperations.execute( new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { entityManager.createQuery( criteriaQuery); //pre-compile critera query to avoid race conditions when setting aliases } }); return criteriaQuery; } /** * Common logic for creating and configuring JPA queries * * @param criteriaQuery The criteria to create the query from */ protected final <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery) { return this.getEntityManager().createQuery(criteriaQuery); } /** * Important common logic for creating and configuring JPA queries cached in EhCache. This step * is important for the sake of scalability. * * @param criteriaQuery The criteria to create a cached query from */ protected final <T> TypedQuery<T> createCachedQuery(CriteriaQuery<T> criteriaQuery) { final TypedQuery<T> query = this.getEntityManager().createQuery(criteriaQuery); final String cacheRegion = getCacheRegionName(criteriaQuery); query.setHint("org.hibernate.cacheable", true); query.setHint("org.hibernate.cacheRegion", cacheRegion); return query; } /** * Utility for creating queries based on naturalId. The caller MUST be annotated with {@link * org.apereo.portal.jpa.OpenEntityManager} or {@link Transactional} so that the Hibernate * specific extensions are available. */ protected final <T> NaturalIdQuery<T> createNaturalIdQuery(Class<T> entityType) { final EntityManager entityManager = this.getEntityManager(); final Session session; try { session = entityManager.unwrap(Session.class); } catch (IllegalStateException e) { throw new IllegalStateException( "The DAO Method that calls createNaturalIdQuery must be annotated with @OpenEntityManager or @Transactional", e); } final NaturalIdLoadAccess naturalIdLoadAccess = session.byNaturalId(entityType); return new NaturalIdQuery<T>(entityType, naturalIdLoadAccess); } /** * Creates the cache region name for the criteria query * * @param criteriaQuery The criteria to create the cache name for */ protected final <T> String getCacheRegionName(CriteriaQuery<T> criteriaQuery) { final Set<Root<?>> roots = criteriaQuery.getRoots(); final Class<?> cacheRegionType = roots.iterator().next().getJavaType(); final String cacheRegion = cacheRegionType.getName() + QUERY_SUFFIX; if (roots.size() > 1) { logger.warn( "Query " + criteriaQuery + " in " + this.getClass() + " has " + roots.size() + " roots. The first will be used to generated the cache region name: " + cacheRegion); } return cacheRegion; } /** * Build a query for an entity using its naturalId * * @param <T> The entity type to return */ public static final class NaturalIdQuery<T> { private final Class<T> type; private final NaturalIdLoadAccess naturalIdLoadAccess; public NaturalIdQuery(Class<T> type, NaturalIdLoadAccess naturalIdLoadAccess) { this.type = type; this.naturalIdLoadAccess = naturalIdLoadAccess; } /** @see NaturalIdLoadAccess#with(LockOptions) */ public NaturalIdQuery<T> with(LockOptions lockOptions) { naturalIdLoadAccess.with(lockOptions); return this; } /** * Set a naturalId parameter using the JPA2 MetaModel API * * @see NaturalIdLoadAccess#using(String, Object) */ public <P> NaturalIdQuery<T> using(Attribute<? super T, P> attribute, P value) { naturalIdLoadAccess.using(attribute.getName(), value); return this; } /** @see NaturalIdLoadAccess#getReference() */ public T getReference() { return type.cast(naturalIdLoadAccess.getReference()); } /** @see NaturalIdLoadAccess#load() */ public T load() { return type.cast(naturalIdLoadAccess.load()); } } }