/* * Copyright 2009-2011 the original author or authors. * * Licensed 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * 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.jdal.dao.jpa; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.persistence.EntityManager; import javax.persistence.Parameter; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Order; import javax.persistence.criteria.Path; import javax.persistence.criteria.Root; import javax.persistence.criteria.Selection; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.Attribute.PersistentAttributeType; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jdal.dao.DaoSupport; import org.jdal.dao.Filter; import org.jdal.dao.Page; import org.jdal.dao.PageableDataSource; import org.jdal.dao.jpa.query.EntityTypeQueryFinder; import org.jdal.dao.jpa.query.QueryFinder; import org.springframework.beans.PropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.dao.InvalidDataAccessApiUsageException; /** * Dao implementation for JPA * * @author Jose Luis Martin * @see org.jdal.dao.Dao * @since 1.1 */ public class JpaDao<T, PK extends Serializable> extends DaoSupport<T, PK> { private static final int DEFAULT_DEPTH = 2; private static final Log log = LogFactory.getLog(JpaDao.class); @PersistenceContext private EntityManager em; private Class<T> entityClass; private Map<String, JpaCriteriaBuilder<?>> criteriaBuilderMap = Collections.synchronizedMap( new HashMap<String, JpaCriteriaBuilder<?>>()); private QueryFinder queryFinder; private boolean onDeleteSetNull = true; /** * Default Ctor, When using it, you need to set entityClass */ public JpaDao() { } /** * Create a new JpaDao for entity class * @param entityClass class to map. */ public JpaDao(Class<T> entityClass) { this.entityClass = entityClass; } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public <K> Page<K> getPage(Page<K> page) { // try named query first TypedQuery<K> query = getNamedQuery(page); if (query == null) // get query from criteria query = getCriteriaQuery(page); // add range query.setMaxResults(page.getPageSize()); query.setFirstResult(page.getStartIndex()); page.setData(query.getResultList()); page.setPageableDataSource(((PageableDataSource<K>) this)); return page; } /** * Build CriteriaQuery using declared JpaCriteriaBuilder in filterMap * @param page * @return a CriteriaQuery from filter */ @SuppressWarnings("unchecked") private <K> CriteriaQuery<K> getCriteria(Page<K> page) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<K> c = (CriteriaQuery<K>) cb.createQuery(); Filter filter = null; if (page.getFilter() instanceof Filter) { filter = (Filter) page.getFilter(); JpaCriteriaBuilder<K> jcb = (JpaCriteriaBuilder<K>) criteriaBuilderMap.get(filter.getFilterName()); if (jcb != null) { if (log.isDebugEnabled()) log.debug("Found JpaCriteriaBuilder for filter: " + filter.getFilterName()); // build criteria c = jcb.build(c, cb, filter); } else { log.error("No CriteriaBuilder found for filter name [" + filter.getFilterName() + "]"); c.from(entityClass); } } else { c.select((Selection<? extends K>) c.from(getEntityClass())); } return c; } /** * Create a TypedQuery from a request page * @param page request page * @return new TypedQuery */ @SuppressWarnings("unchecked") private <K> TypedQuery<K> getCriteriaQuery(Page<K> page) { CriteriaQuery<K> criteria = getCriteria(page); CriteriaQuery<Long> countCriteria = (CriteriaQuery<Long>) getCriteria(page); CriteriaBuilder cb = em.getCriteriaBuilder(); Root<?> root = countCriteria.getRoots().iterator().next(); countCriteria.select(cb.count(root)); page.setCount((em.createQuery(countCriteria).getSingleResult()) .intValue()); criteria.orderBy(getOrder(page, criteria)); // Add default select to entity class if none was set. if (criteria.getSelection() == null) { criteria.select((Selection<? extends K>) root); } return em.createQuery(criteria); } /** * {@inheritDoc} */ public List<Serializable> getKeys(Page<T> page) { Filter filter = null; TypedQuery<Serializable> query = null; SingularAttribute<? super T, ?> id = getIdAttribute(); // try named query if (page.getFilter() instanceof Filter) { filter = (Filter) page.getFilter(); String queryString = getQueryString(filter.getFilterName()); if (queryString != null) { String keyQuery = JpaUtils.getKeyQuery(queryString, id.getName()); // add Order if (page.getSortName() != null) keyQuery = JpaUtils.addOrder(keyQuery, page.getSortName(), page.getOrder() == Page.Order.ASC); query = em.createQuery(keyQuery, Serializable.class); applyFilter(query, filter); } else { query = getKeyCriteriaQuery(id, page); } } else { query = getKeyCriteriaQuery(id, page); } query.setFirstResult(page.getStartIndex()); query.setMaxResults(page.getPageSize()); return query.getResultList(); } /** * Gets CriteriaQuery for Page Keys * @param page * @return CriteriaQuery */ @SuppressWarnings("unchecked") private TypedQuery<Serializable> getKeyCriteriaQuery(SingularAttribute<? super T, ?> id, Page<T> page) { CriteriaQuery<Serializable> keyCriteria = (CriteriaQuery<Serializable>) getCriteria(page); Root<T> keyRoot = JpaUtils.findRoot(keyCriteria, getEntityClass()); keyCriteria.select(keyRoot.<Serializable>get(id.getName())); return em.createQuery(keyCriteria); } /** * Gets de id attribute from metamodel * @return PK SingularAttribute */ private SingularAttribute<? super T, ?> getIdAttribute() { return getIdAttribute(getEntityClass()); } /** * @param class1 * @return */ private <K> SingularAttribute<? super K, ?> getIdAttribute(Class<K> clazz) { Type<?> type = em.getMetamodel().entity(clazz).getIdType(); EntityType<K> entity = em.getMetamodel().entity(clazz); SingularAttribute<? super K, ?> id = entity.getId(type.getJavaType()); return id; } /** * Get JPA Order from a page order for a CriteriaQuery * @param page request page * @param criteria CriteriaQuery to apply Order on. * @return new Order */ private Order getOrder(Page<?> page, CriteriaQuery<?> criteria) { CriteriaBuilder cb = em.getCriteriaBuilder(); Root<T> root = JpaUtils.findRoot(criteria, getEntityClass()); String propertyPath = page.getSortName(); if (log.isDebugEnabled()) log.debug("Setting order as: " + propertyPath); Path<?> path = JpaUtils.getPath(root, propertyPath); return page.getOrder() == Page.Order.ASC ? cb.asc(path) : cb.desc(path); } /** * Gets a NamedQuery from page, setup order, params and page result count. * @param page request page * @return a TypedQuery from a NamedQuery */ @SuppressWarnings("unchecked") protected <K> TypedQuery<K> getNamedQuery(Page<K> page) { Filter filter = null; TypedQuery<K> query = null; if (page.getFilter() instanceof Filter) { filter = (Filter) page.getFilter(); String queryString = getQueryString(filter.getFilterName()); if (queryString != null) { String countQueryString = JpaUtils.createCountQueryString(queryString); TypedQuery<Long> countQuery = em.createQuery(countQueryString, Long.class); applyFilter(countQuery, filter); page.setCount(countQuery.getSingleResult().intValue()); // add Order if (page.getSortName() != null) queryString = JpaUtils.addOrder(queryString, page.getSortName(), page.getOrder() == Page.Order.ASC); query = (TypedQuery<K>) em.createQuery(queryString); applyFilter(query, filter); } } return query; } /** * Apply filter to parametriced Query * @param query the query to apply filter on * @param filter Filter to apply */ private void applyFilter(Query query, Filter filter) { Map<String, Object> parameterMap = filter.getParameterMap(); for (Parameter<?> p : query.getParameters()) { if (parameterMap.containsKey(p.getName())) { query.setParameter(p.getName(), parameterMap.get(p.getName())); } else { throw new InvalidDataAccessApiUsageException("Parameter " + p.getName() + "was not found in filter " + filter.getFilterName() + ". Review NamedQuery or Filter."); } } } /** * Gets query string fro named query using configured QueryFinder, if it's null * use EntityTypeQueryFinder as defaults (looks for anntations on entity class). * @param name query name * @return query string. */ protected String getQueryString(String name) { if (queryFinder == null) queryFinder = new EntityTypeQueryFinder(em.getMetamodel().entity(getEntityClass())); return queryFinder.find(name); } /** * {@inheritDoc} */ public List<T> findByNamedQuery(String queryName, Map<String, Object> queryParams) { TypedQuery<T> query = em.createNamedQuery(queryName, getEntityClass()); if (queryParams != null) { for (Map.Entry<String, ?> entry : queryParams.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } return query.getResultList(); } /** * {@inheritDoc} */ public T get(PK id) { return em.find(getEntityClass(), id); } /** * {@inheritDoc} */ public List<T> getAll() { CriteriaQuery<T> q = em.getCriteriaBuilder().createQuery(getEntityClass()); q.from(getEntityClass()); return em.createQuery(q).getResultList(); } /** * {@inheritDoc} */ public void delete(T entity) { if (!em.contains(entity)) entity = em.merge(entity); if (onDeleteSetNull) nullReferences(entity); em.remove(entity); } /** * {@inheritDoc} */ public void deleteById(PK id) { delete(get(id)); } /** * Null References on one to many and one to one associations. * Will only work if association has annotated with a mappedBy attribute. * * @param entity entity */ private void nullReferences(T entity) { EntityType<T> type = em.getMetamodel().entity(getEntityClass()); if (log.isDebugEnabled()) log.debug("Null references on entity " + type.getName()); for (Attribute<?, ?> a : type.getAttributes()) { if (PersistentAttributeType.ONE_TO_MANY == a.getPersistentAttributeType() || PersistentAttributeType.ONE_TO_ONE == a.getPersistentAttributeType()) { Object association = PropertyAccessorFactory.forDirectFieldAccess(entity) .getPropertyValue(a.getName()); if (association != null) { EntityType<?> associationType = null; if (a.isCollection()) { associationType = em.getMetamodel().entity( ((PluralAttribute<?, ?, ?>)a).getBindableJavaType()); } else { associationType = em.getMetamodel().entity(a.getJavaType()); } String mappedBy = JpaUtils.getMappedBy(a); if (mappedBy != null) { Attribute<?,?> aa = associationType.getAttribute(mappedBy); if (PersistentAttributeType.MANY_TO_ONE == aa.getPersistentAttributeType()) { if (log.isDebugEnabled()) { log.debug("Null ManyToOne reference on " + associationType.getName() + "." + aa.getName()); } for (Object o : (Collection<?>) association) { PropertyAccessorFactory.forDirectFieldAccess(o).setPropertyValue(aa.getName(), null); } } else if (PersistentAttributeType.ONE_TO_ONE == aa.getPersistentAttributeType()) { if (log.isDebugEnabled()) { log.debug("Null OneToOne reference on " + associationType.getName() + "." + aa.getName()); } PropertyAccessorFactory.forDirectFieldAccess(association).setPropertyValue(aa.getName(), null); } } } } } } /** * {@inheritDoc} */ public boolean exists(PK id) { return id != null && get(id) != null; } /** * {@inheritDoc} */ public T initialize(T entity) { initialize(entity, DEFAULT_DEPTH); return entity; } /** * {@inheritDoc} */ public T initialize(T entity, int depth) { JpaUtils.initialize(em, entity, depth); return entity; } /** * {@inheritDoc} */ public T save(T entity) { T persistentEntity; if (isNew(entity)) { em.persist(entity); persistentEntity = entity; } else { persistentEntity = em.merge(entity); } return persistentEntity; } /** * Test if entity is New * @param entity * @return true if entity is new, ie not detached */ @SuppressWarnings("unchecked") protected boolean isNew(T entity) { SingularAttribute<?, ?> id = getIdAttribute(entity.getClass()); // try field PropertyAccessor pa = PropertyAccessorFactory.forDirectFieldAccess(entity); PK key = (PK) pa.getPropertyValue(id.getName()); if (key == null) key = (PK) PropertyAccessorFactory.forBeanPropertyAccess(entity).getPropertyValue(id.getName()); return key == null || !exists(key, entity.getClass()); } /** * @param key * @param class1 * @return */ private boolean exists(PK key, Class<? extends Object> clazz) { return get(key, clazz) != null; } /** * @return entity class */ public Class<T> getEntityClass() { return entityClass; } /** * @return the criteriaBuilderMap */ public Map<String, JpaCriteriaBuilder<?>> getCriteriaBuilderMap() { return criteriaBuilderMap; } /** * @param criteriaBuilderMap the criteriaBuilderMap to set */ public void setCriteriaBuilderMap(Map<String, JpaCriteriaBuilder<T>> criteriaBuilderMap) { this.criteriaBuilderMap.clear(); this.criteriaBuilderMap.putAll(criteriaBuilderMap); } /** * @return the queryFinder */ public QueryFinder getQueryFinder() { return queryFinder; } /** * @param queryFinder the queryFinder to set */ public void setQueryFinder(QueryFinder queryFinder) { this.queryFinder = queryFinder; } /** * {@inheritDoc} */ public <E> E get(PK id, Class<E> clazz) { return em.find(clazz, id); } /** * {@inheritDoc} */ public <E> List<E> getAll(Class<E> clazz) { CriteriaQuery<E> q = em.getCriteriaBuilder().createQuery(clazz); q.from(clazz); return em.createQuery(q).getResultList(); } /** * @return the em */ public EntityManager getEntityManager() { return em; } /** * @param em the em to set */ public void setEntityManager(EntityManager em) { this.em = em; } /** * @param entityClass the entityClass to set */ public void setEntityClass(Class<T> entityClass) { this.entityClass = entityClass; } /** * @return the onDeleteSetNull */ public boolean isOnDeleteSetNull() { return onDeleteSetNull; } /** * @param onDeleteSetNull the onDeleteSetNull to set */ public void setOnDeleteSetNull(boolean onDeleteSetNull) { this.onDeleteSetNull = onDeleteSetNull; } }