/*
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.dao;
import java.io.InputStream;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nl.strohalm.cyclos.entities.Entity;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.exceptions.DaoException;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.exceptions.ApplicationException;
import nl.strohalm.cyclos.utils.ClassHelper;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.EntityHelper;
import nl.strohalm.cyclos.utils.JDBCWrapper;
import nl.strohalm.cyclos.utils.hibernate.HibernateHelper;
import nl.strohalm.cyclos.utils.hibernate.HibernateQueryHandler;
import nl.strohalm.cyclos.utils.query.PageParameters;
import nl.strohalm.cyclos.utils.query.QueryParameters;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import org.apache.commons.lang.ArrayUtils;
import org.hibernate.Cache;
import org.hibernate.HibernateException;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.jdbc.Work;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
/**
* Basic implementation for DAOs, extending Spring Framework support for Hibernate 3.
*
* @author rafael
* @author Ivan "Fireblade" Diana
*/
public abstract class BaseDAOImpl<E extends Entity> extends HibernateDaoSupport implements BaseDAO<E>, InsertableDAO<E>, UpdatableDAO<E>, DeletableDAO<E> {
private FetchDAO fetchDao;
private HibernateQueryHandler hibernateQueryHandler;
private boolean hasCache;
protected Class<E> entityClass;
private String queryCacheRegion;
public BaseDAOImpl(final Class<E> entityClass) {
this.entityClass = entityClass;
}
@Override
public Blob createBlob(final InputStream stream, final int length) {
return getHibernateTemplate().execute(new HibernateCallback<Blob>() {
@Override
public Blob doInHibernate(final Session session) throws HibernateException, SQLException {
return session.getLobHelper().createBlob(stream, length);
}
});
}
@Override
public int delete(final boolean flush, final Long... ids) {
try {
if (ids != null && ids.length > 0) {
int count = 0;
for (final Long id : ids) {
final Object element = getHibernateTemplate().get(getEntityType(), id);
if (element != null) {
getHibernateTemplate().delete(element);
count++;
}
}
if (count > 0 && flush) {
flush();
}
// Ensure the second level cache is not getting stale
evictSecondLevelCache();
return count;
}
return 0;
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
@Override
public final int delete(final Long... ids) throws DaoException {
return delete(true, ids);
}
@Override
@SuppressWarnings("unchecked")
public <T extends E> T duplicate(final T entity) {
if (entity == null) {
return null;
}
final T duplicate = (T) ClassHelper.instantiate(entity.getClass());
hibernateQueryHandler.copyProperties(entity, duplicate);
return duplicate;
}
@Override
public Class<E> getEntityType() {
return this.entityClass;
}
public FetchDAO getFetchDao() {
return fetchDao;
}
public HibernateQueryHandler getHibernateQueryHandler() {
return hibernateQueryHandler;
}
@Override
public final <T extends E> T insert(final T entity) throws UnexpectedEntityException, DaoException {
return insert(entity, true);
}
@Override
public <T extends E> T insert(final T entity, final boolean flush) {
try {
if (entity == null || entity.isPersistent()) {
throw new UnexpectedEntityException();
}
hibernateQueryHandler.resolveReferences(entity);
getHibernateTemplate().save(entity);
if (flush) {
flush();
}
// Ensure the second level cache is not getting stale
evictSecondLevelCache();
return entity;
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
@Override
public <T extends E> Collection<T> load(final Collection<Long> ids, final Relationship... fetch) {
if (ids == null) {
return null;
}
final Collection<T> toReturn = new ArrayList<T>();
for (final Long id : ids) {
T entity = this.<T> load(id, fetch);
toReturn.add(entity);
}
return toReturn;
}
@Override
@SuppressWarnings("unchecked")
public <T extends E> T load(final Long id, final Relationship... fetch) {
if (id == null) {
throw new EntityNotFoundException(getEntityType());
}
try {
// Determine the best way to load an entity.
// 1. No second level cache and fetch is used: hql query - bypasses the cache, but can include relationships in a single select
// 2. With cache: load - probably a cache hit. Then fetch the relationships if any
T entity = null;
if (!hasCache && !ArrayUtils.isEmpty(fetch)) {
// Perform a query
final Map<String, Object> namedParams = new HashMap<String, Object>();
final StringBuilder hql = HibernateHelper.getInitialQuery(getEntityType(), "e", Arrays.asList(fetch));
HibernateHelper.addParameterToQuery(hql, namedParams, "e.id", id);
final List<E> list = list(ResultType.LIST, hql.toString(), namedParams, PageParameters.unique(), fetch);
if (list.isEmpty()) {
throw new EntityNotFoundException(this.getEntityType(), id);
} else {
// We must call the fetch DAO anyway because there may be other fetches not retrieved by the hql
entity = (T) list.iterator().next();
}
} else {
// Perform a normal load
try {
// Although there are no fetch relationships we must call the fetch DAO
// to initialize the entity itself
entity = (T) getHibernateTemplate().load(getEntityType(), id);
} catch (final ObjectNotFoundException e) {
throw new EntityNotFoundException(this.getEntityType(), id);
}
}
return fetchDao.fetch(entity, fetch);
} catch (final ApplicationException e) {
throw e;
} catch (final ObjectNotFoundException e) {
throw new EntityNotFoundException(getEntityType(), id);
} catch (final Exception e) {
throw new DaoException(e);
}
}
@Override
@SuppressWarnings("unchecked")
// TODO: Review if this implementation is OK
public <T extends E> T reload(final Long id, final Relationship... fetch) {
if (id == null) {
return null;
}
final T entity = (T) EntityHelper.reference(getEntityType(), id);
return fetchDao.reload(entity, fetch);
}
public final void setFetchDao(final FetchDAO fetchDao) {
this.fetchDao = fetchDao;
}
public void setHibernateQueryHandler(final HibernateQueryHandler hibernateQueryHandler) {
this.hibernateQueryHandler = hibernateQueryHandler;
}
@Override
public final <T extends E> T update(final T entity) throws DaoException {
return update(entity, true);
}
@Override
@SuppressWarnings("unchecked")
public <T extends E> T update(final T entity, final boolean flush) {
if (entity == null || entity.isTransient()) {
throw new UnexpectedEntityException();
}
try {
hibernateQueryHandler.resolveReferences(entity);
final T ret = getHibernateTemplate().execute(new HibernateCallback<T>() {
@Override
public T doInHibernate(final Session session) throws HibernateException, SQLException {
// As hibernate can have only 1 instance with a given id, if another instance with that id
// is passed to update, it will throw a NonUniqueObjectException. So, we must merge the data
try {
session.update(entity);
return entity;
} catch (final NonUniqueObjectException e) {
// If there is another instance associated with the same id,
// merge the data on the associated instance...
session.merge(entity);
final Entity current = (Entity) session.load(EntityHelper.getRealClass(entity), entity.getId());
// hibernateQueryHandler.copyProperties(entity, current);
// ... and return it
return (T) current;
}
}
});
if (flush) {
flush();
}
// Ensure the second level cache is not getting stale
evictSecondLevelCache();
return ret;
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
/**
* Executes a bulk update
*/
protected int bulkUpdate(final String hql, final Object namedParameters) {
try {
return getHibernateTemplate().execute(new HibernateCallback<Integer>() {
@Override
public Integer doInHibernate(final Session session) throws HibernateException, SQLException {
final Query query = session.createQuery(hql);
hibernateQueryHandler.setQueryParameters(query, namedParameters);
int rows = query.executeUpdate();
if (rows > 0) {
CurrentTransactionData.setWrite();
}
return rows;
}
});
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
@Override
protected HibernateTemplate createHibernateTemplate(final SessionFactory sessionFactory) {
// Retrieve whether this entity uses the second level cache or not
final SessionFactoryImplementor sf = (SessionFactoryImplementor) sessionFactory;
hasCache = sf.getEntityPersister(getEntityType().getName()).hasCache();
if (shouldUseQueryCache()) {
queryCacheRegion = "query." + getClass().getSimpleName();
}
return super.createHibernateTemplate(sessionFactory);
}
/**
* Evicts all second-level cache elements which could get stale on entity updates
*/
protected void evictSecondLevelCache() {
final SessionFactory sessionFactory = getSessionFactory();
// If this DAO is cached, evict the collection regions, as we don't know which ones will point out to it
if (hasCache) {
synchronized (sessionFactory) {
final Cache cache = sessionFactory.getCache();
// We must invalidate all collection regions, as we don't know which other entities have many-to-many relationships with this one
cache.evictCollectionRegions();
}
}
// Evict the query cache region
if (queryCacheRegion != null) {
synchronized (sessionFactory) {
final Cache cache = sessionFactory.getCache();
cache.evictQueryRegion(queryCacheRegion);
}
}
}
/**
* Flushes the hibernate session
*/
protected void flush() {
getHibernateTemplate().flush();
}
/**
* Creates an iterator
*/
protected <T> Iterator<T> iterate(final String hql, final Object namedParameters) {
try {
return hibernateQueryHandler.simpleIterator(hql, namedParameters, null);
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
/**
* Executes a query with minimal parameters, treating the query object as the source for named parameters
* @param query The query parameters contains the result type, named parameters as bean properties, the pagination parameters and the properties
* to fetch
* @param hql The HQL query
* @return A list, as returned by {@link HibernateQueryHandler}
*/
protected <T> List<T> list(final QueryParameters query, final String hql) {
return list(query, hql, query);
}
/**
* Executes a query with a query parameters and a separate named parameters object
* @param query The query parameters contains the result type, the pagination parameters and the properties to fetch
* @param hql The HQL query
* @return A list, as returned by {@link HibernateQueryHandler}
*/
protected <T> List<T> list(final QueryParameters query, final String hql, final Object namedParameters) {
return list(query.getResultType(), hql, namedParameters, query.getPageParameters(), fetchArray(query));
}
/**
* Execute a list with all possible parameters
* @param resultType The expected result type
* @param hql The HQL query
* @param namedParameters The HQL named parameters - May be a Map or a Bean
* @param pageParameters The pagination parameters, if any. It affects any ResultType, by limiting the number of results the same way as pages
* @param fetch The relationships to fetch
* @return A list, as returned by {@link HibernateQueryHandler}
*/
protected <T> List<T> list(final ResultType resultType, final String hql, final Object namedParameters, final PageParameters pageParameters, final Relationship... fetch) {
try {
return hibernateQueryHandler.executeQuery(queryCacheRegion, resultType, hql, namedParameters, pageParameters, fetch);
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
/**
* Execute a simple list, binding parameters to the query
*/
@SuppressWarnings("unchecked")
protected <T> List<T> list(final String hql, final Object namedParameters) {
try {
return getHibernateTemplate().executeFind(new HibernateCallback<List<T>>() {
@Override
public List<T> doInHibernate(final Session session) throws HibernateException, SQLException {
final Query query = session.createQuery(hql);
process(query, namedParameters);
return query.list();
}
});
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
/**
* Executes a query, returning results as a Map. The result is expected to be an array, where the first element is the key and the second is the
* value
*/
@SuppressWarnings("unchecked")
protected <K, V> Map<K, V> map(final String hql, final Object namedParameters) {
Map<K, V> map = new LinkedHashMap<K, V>();
Iterator<Object[]> iterator = this.<Object[]> iterate(hql, namedParameters);
try {
while (iterator.hasNext()) {
Object[] row = iterator.next();
map.put((K) row[0], (V) row[1]);
}
} finally {
DataIteratorHelper.close(iterator);
}
return map;
}
/**
* Runs something directly in the database connection using a {@link JDBCCallback}
*/
protected void runNative(final JDBCCallback callback) {
getSession().doWork(new Work() {
@Override
public void execute(final Connection connection) throws SQLException {
callback.execute(new JDBCWrapper(connection));
}
});
// As there is no way to know whether the native execution performed writes, assume yes
CurrentTransactionData.setWrite();
}
/**
* May be overridden in order to determine whether the query cache will be used
*/
protected boolean shouldUseQueryCache() {
// By default, use the query cache when the second level cache is enabled
return hasCache;
}
/**
* Execute a simple unique result, binding parameters to the query
*/
@SuppressWarnings("unchecked")
protected <T> T uniqueResult(final String hql, final Object namedParameters) {
try {
return getHibernateTemplate().execute(new HibernateCallback<T>() {
@Override
public T doInHibernate(final Session session) throws HibernateException, SQLException {
final Query query = session.createQuery(hql);
process(query, namedParameters);
query.setMaxResults(1);
return (T) query.uniqueResult();
}
});
} catch (final ApplicationException e) {
throw e;
} catch (final Exception e) {
throw new DaoException(e);
}
}
private Relationship[] fetchArray(final QueryParameters query) {
Relationship[] fetch;
if (query.getFetch() == null || query.getFetch().isEmpty()) {
fetch = new Relationship[0];
} else {
fetch = query.getFetch().toArray(new Relationship[query.getFetch().size()]);
}
return fetch;
}
private void process(final Query query, final Object namedParameters) {
hibernateQueryHandler.setQueryParameters(query, namedParameters);
if (queryCacheRegion != null) {
query.setCacheable(true);
query.setCacheRegion(queryCacheRegion);
}
}
}