/* This file is part of Cyclos (www.cyclos.org). A project of the Social Trade Organisation (www.socialtrade.org). Cyclos is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Cyclos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Cyclos; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package nl.strohalm.cyclos.utils.hibernate; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import nl.strohalm.cyclos.dao.FetchDAO; import nl.strohalm.cyclos.entities.Entity; import nl.strohalm.cyclos.entities.EntityReference; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.utils.ClassHelper; import nl.strohalm.cyclos.utils.EntityHelper; import nl.strohalm.cyclos.utils.FetchingIteratorListImpl; import nl.strohalm.cyclos.utils.IteratorListImpl; import nl.strohalm.cyclos.utils.PropertyHelper; import nl.strohalm.cyclos.utils.ScrollableResultsIterator; import nl.strohalm.cyclos.utils.query.IteratorList; import nl.strohalm.cyclos.utils.query.Page; import nl.strohalm.cyclos.utils.query.PageImpl; import nl.strohalm.cyclos.utils.query.PageParameters; import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType; import org.apache.commons.lang.StringUtils; import org.hibernate.EntityMode; import org.hibernate.Hibernate; import org.hibernate.HibernateException; import org.hibernate.ObjectNotFoundException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.collection.PersistentCollection; import org.hibernate.metadata.ClassMetadata; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; import org.hibernate.type.EntityType; import org.hibernate.type.MapType; import org.hibernate.type.Type; import org.springframework.orm.hibernate3.HibernateCallback; import org.springframework.orm.hibernate3.HibernateTemplate; /** * Handler for entity queries using Hibernate * @author luis */ public class HibernateQueryHandler { private static Pattern FIRST_ALIAS = Pattern.compile("^ *(from +[^ ]+|select(?: +distinct)?) +([^ ]+).*"); private static Pattern LEFT_JOIN_FETCH = Pattern.compile("\\^left\\s+join\\s+fetch(\\s+[\\w\\.]+)?(\\s*[\\w]+)?"); /** * Returns an HQL query without the fetch part */ private static String stripFetch(String hql) { // This is done so we don't confuse the matcher, i.e.: from X x left join fetch x.a *left* join fetch ... -> that *left* could be an alias hql = hql.replaceAll("left join", "^left join"); Matcher matcher = LEFT_JOIN_FETCH.matcher(hql); StringBuffer sb = new StringBuffer(); while (matcher.find()) { String path = StringUtils.trimToEmpty(matcher.group(1)); String alias = StringUtils.trimToEmpty(matcher.group(2)); boolean nextIsLeft = "left".equalsIgnoreCase(alias); boolean safeToRemove = alias.isEmpty() || nextIsLeft || "where".equalsIgnoreCase(alias) || "order".equalsIgnoreCase(alias) || "group".equalsIgnoreCase(alias); String replacement; if (safeToRemove) { // No alias - we can just remove the entire left join fetch replacement = nextIsLeft ? "" : " " + alias; } else { // Just remove the 'fetch' replacement = " left join " + path + " " + alias; } matcher.appendReplacement(sb, replacement); } matcher.appendTail(sb); return sb.toString().replaceAll("\\^left join", "left join"); } /** * Return a count HQL. Should handle fetches and order by, removing them */ private static String transformToCount(String hql) { hql = stripFetch(hql); final int fromIndex = hql.indexOf("from "); final int orderIndex = hql.indexOf("order by "); final StringBuilder sb = new StringBuilder(); final boolean isDistinct = hql.indexOf(" distinct ") >= 0; final Matcher matcher = FIRST_ALIAS.matcher(hql); if (matcher.matches()) { final String firstAlias = matcher.group(2); sb.append("select count(").append(isDistinct ? " distinct " : "").append(firstAlias).append(".id) "); } else { sb.append("select count(*)"); } if (orderIndex < 0) { // select a from A a where 1=1 // -> select count(*) from A a where 1=1 sb.append(hql.substring(fromIndex)); } else { // select a, b from A a where 1=1 order by a.name // -> select count(*) from A a where 1=1 sb.append(hql.substring(fromIndex, orderIndex)); } return sb.toString(); } private FetchDAO fetchDao; private HibernateTemplate hibernateTemplate; /** * Applies the result limits to the given query */ public void applyPageParameters(final PageParameters pageParameters, final Query query) { Integer firstResult = pageParameters == null ? null : pageParameters.getFirstResult(); if (firstResult != null && firstResult >= 0) { query.setFirstResult(firstResult); } Integer maxResults = pageParameters == null ? null : pageParameters.getMaxResults(); if (maxResults != null && maxResults > 0) { query.setMaxResults(maxResults); } } /** * Apply the page parameters to an in-memory collection */ public <E> List<E> applyResultParameters(final ResultType resultType, final PageParameters pageParameters, final Collection<E> records) { switch (resultType) { case LIST: case ITERATOR: final int maxResults = pageParameters == null ? Integer.MAX_VALUE : pageParameters.getMaxResults(); List<E> list = new ArrayList<E>(); if (maxResults > 0) { int i = 0; for (final E item : records) { list.add(item); i++; if (i >= maxResults) { break; } } } if (resultType == ResultType.ITERATOR) { list = new IteratorListImpl<E>(list.iterator()); } return list; case PAGE: final int currentPage = pageParameters == null ? 0 : pageParameters.getCurrentPage(); final int pageSize = pageParameters == null ? 15 : pageParameters.getPageSize(); final int firstIndex = currentPage * pageSize; final int lastIndex = firstIndex + pageSize; final List<E> pageElements = new ArrayList<E>(pageSize); int index = -1; for (final E payment : records) { index++; if (index < firstIndex) { continue; } if (index > lastIndex) { break; } pageElements.add(payment); } return new PageImpl<E>(pageParameters, records.size(), pageElements); default: throw new IllegalStateException(resultType + "?"); } } /** * Copies the persistent properties from the source to the destination entity */ public void copyProperties(final Entity source, final Entity dest) { if (source == null || dest == null) { return; } final ClassMetadata metaData = getClassMetaData(source); final Object[] values = metaData.getPropertyValues(source, EntityMode.POJO); // Skip the collections final Type[] types = metaData.getPropertyTypes(); for (int i = 0; i < types.length; i++) { final Type type = types[i]; if (type instanceof CollectionType) { values[i] = null; } } metaData.setPropertyValues(dest, values, EntityMode.POJO); } /** * Execute a query based on the parameters * @param <E> The entity type * @param resultType The desired result type * @param hql The HQL query * @param namedParameters The HQL named parameter values (normally a Map or a Bean) * @param pageParameters The page parameters. Affects all ResultTypes, by limiting the number of results * @param fetch The relationships to fetch, if any * @return A List of the expected entity type. It will be: * <ul> * <li>A {@link Page} when ResultType == PAGE</li> * <li>A {@link IteratorList} when ResultType == ITERATOR</li> * <li>A {@link List} when ResultType == LIST</li> * </ul> */ public <E> List<E> executeQuery(final String cacheRegion, final ResultType resultType, final String hql, final Object namedParameters, final PageParameters pageParameters, final Relationship... fetch) { // Check the result type switch (resultType) { case LIST: return list(cacheRegion, hql, namedParameters, pageParameters, fetch); case PAGE: return page(cacheRegion, hql, namedParameters, pageParameters, fetch); case ITERATOR: // Iterators shouldn't use cache to avoid too many objects in memory return iterator(hql, namedParameters, pageParameters, fetch); default: throw new IllegalArgumentException("Unknown result type: " + resultType); } } /** * Returns the class meta data for the given entity */ public ClassMetadata getClassMetaData(final Entity entity) { return hibernateTemplate.getSessionFactory().getClassMetadata(EntityHelper.getRealClass(entity)); } public FetchDAO getFetchDao() { return fetchDao; } /** * Initialize an entity or collection */ public Object initialize(final Object object) { if (object instanceof HibernateProxy) { // Reassociate the entity with the current session Entity entity = (Entity) object; entity = getHibernateTemplate().load(EntityHelper.getRealClass(entity), entity.getId()); // Return the implementation associated with the proxy if (entity instanceof HibernateProxy) { final LazyInitializer lazyInitializer = ((HibernateProxy) entity).getHibernateLazyInitializer(); lazyInitializer.initialize(); return lazyInitializer.getImplementation(); } else { return entity; } } else if (object instanceof PersistentCollection) { // Reassociate the collection with the current session return getHibernateTemplate().execute(new HibernateCallback<Object>() { @Override public Object doInHibernate(final Session session) throws HibernateException, SQLException { final PersistentCollection persistentCollection = ((PersistentCollection) object); Entity owner = (Entity) persistentCollection.getOwner(); final String role = persistentCollection.getRole(); if (owner == null || role == null) { return persistentCollection; } // Retrieve the owner of this persistent collection, associated with the current session owner = (Entity) session.get(EntityHelper.getRealClass(owner), owner.getId()); // Retrieve the collection through it's role (property name) final String propertyName = PropertyHelper.lastProperty(role); final Object currentCollection = PropertyHelper.get(owner, propertyName); if (currentCollection instanceof PersistentCollection) { Hibernate.initialize(currentCollection); } return currentCollection; } }); } try { Hibernate.initialize(object); } catch (final ObjectNotFoundException e) { throw new EntityNotFoundException(); } return object; } /** * Initializes a lazy property */ public Object initializeProperty(final Object bean, final String relationshipName) { final String first = PropertyHelper.firstProperty(relationshipName); Object value = PropertyHelper.get(bean, first); value = initialize(value); PropertyHelper.set(bean, first, value); return value; } @SuppressWarnings("unchecked") public void resolveReferences(final Entity entity) { final ClassMetadata meta = getClassMetaData(entity); final String[] names = meta.getPropertyNames(); final Type[] types = meta.getPropertyTypes(); for (int i = 0; i < types.length; i++) { final Type type = types[i]; final String name = names[i]; if (type instanceof EntityType) { // Properties that are relationships to other entities Entity rel = PropertyHelper.get(entity, name); if (rel instanceof EntityReference) { rel = getHibernateTemplate().load(EntityHelper.getRealClass(rel), rel.getId()); PropertyHelper.set(entity, name, rel); } } else if (type instanceof CollectionType && !(type instanceof MapType)) { // Properties that are collections of other entities final Collection<?> current = PropertyHelper.get(entity, name); if (current != null && !(current instanceof PersistentCollection)) { // We must check that the collection is made of entities, since Hibernate supports collections os values boolean isEntityCollection = true; final Collection<Entity> resolved = ClassHelper.instantiate(current.getClass()); for (final Object object : current) { if (object != null && !(object instanceof Entity)) { isEntityCollection = false; break; } Entity e = (Entity) object; if (object instanceof EntityReference) { e = getHibernateTemplate().load(EntityHelper.getRealClass(e), e.getId()); } resolved.add(e); } if (isEntityCollection) { PropertyHelper.set(entity, name, resolved); } } } } } public void setFetchDao(final FetchDAO fetchDao) { this.fetchDao = fetchDao; } /** * Sets the query bind named parameters */ public void setQueryParameters(final Query query, final Object parameters) { if (parameters != null) { if (parameters instanceof Map<?, ?>) { final Map<?, ?> map = (Map<?, ?>) parameters; final String[] paramNames = query.getNamedParameters(); for (final String param : paramNames) { final Object value = map.get(param); if (value instanceof Collection<?>) { final Collection<Object> values = new ArrayList<Object>(((Collection<?>) value).size()); for (final Object object : (Collection<?>) value) { if (object instanceof EntityReference) { values.add(fetchDao.fetch((Entity) object)); } else { values.add(object); } } query.setParameterList(param, values); } else if (value instanceof EntityReference) { query.setParameter(param, fetchDao.fetch((Entity) value)); } else { query.setParameter(param, value); } } } else { query.setProperties(parameters); } } } public void setSessionFactory(final SessionFactory sessionFactory) { hibernateTemplate = new HibernateTemplate(sessionFactory); } /** * Iterate the query with hibernate */ public <E> Iterator<E> simpleIterator(final String hql, final Object namedParameters, final PageParameters pageParameters) { final Iterator<E> iterator = getHibernateTemplate().execute(new HibernateCallback<Iterator<E>>() { @Override public Iterator<E> doInHibernate(final Session session) throws HibernateException { // Iterators cannot have fetch on HQL String strippedHql = stripFetch(hql); final Query query = session.createQuery(strippedHql); applyPageParameters(pageParameters, query); setQueryParameters(query, namedParameters); return new ScrollableResultsIterator<E>(query, null); } }); return iterator; } /** * List the query with hibernate */ @SuppressWarnings("unchecked") public <E> List<E> simpleList(final String cacheRegion, final String hql, final Object namedParameters, final PageParameters pageParameters, final Relationship... fetch) { final List<E> list = getHibernateTemplate().execute(new HibernateCallback<List<E>>() { @Override public List<E> doInHibernate(final Session session) throws HibernateException { final Query query = session.createQuery(hql); setQueryParameters(query, namedParameters); applyPageParameters(pageParameters, query); if (cacheRegion != null) { query.setCacheable(true); query.setCacheRegion(cacheRegion); } return query.list(); } }); if (fetch != null && fetch.length > 0) { for (int i = 0; i < list.size(); i++) { final Entity entity = (Entity) list.get(i); list.set(i, (E) fetchDao.fetch(entity, fetch)); } } return list; } private HibernateTemplate getHibernateTemplate() { return hibernateTemplate; } /** * Execute the query for ResultType == ITERATOR */ private <E> List<E> iterator(final String hql, final Object namedParameters, final PageParameters pageParameters, final Relationship[] fetch) { final Iterator<E> iterator = simpleIterator(hql, namedParameters, pageParameters); return new FetchingIteratorListImpl<E>(iterator, fetchDao, fetch); } /** * Execute the query for ResultType == LIST * @param pageParameters */ private <E> List<E> list(final String cacheRegion, final String hql, final Object namedParameters, final PageParameters pageParameters, final Relationship... fetch) { return simpleList(cacheRegion, hql, namedParameters, pageParameters, fetch); } /** * Execute the query for ResultType == PAGE */ private <E> List<E> page(final String cacheRegion, final String hql, final Object namedParameters, final PageParameters pageParameters, final Relationship[] fetch) { // Count all records final Integer totalCount = getHibernateTemplate().execute(new HibernateCallback<Integer>() { @Override public Integer doInHibernate(final Session session) throws HibernateException { final Query query = session.createQuery(transformToCount(hql.toString())); setQueryParameters(query, namedParameters); setCacheRegion(query, cacheRegion); return (Integer) query.uniqueResult(); } }); // Get only the page records List<E> list; if (pageParameters.getMaxResults() == 0) { // Max results == 0 means only to count list = Collections.emptyList(); } else { list = simpleList(cacheRegion, hql, namedParameters, pageParameters, fetch); } // Create a page instance return new PageImpl<E>(pageParameters, totalCount, list); } private void setCacheRegion(final Query query, final String cacheRegion) { if (cacheRegion != null) { query.setCacheable(true); query.setCacheRegion(cacheRegion); } } }