// $HeadURL$
// $Id$
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.db.datafetcher;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.apache.log4j.Logger;
import org.hibernate.Session;
import edu.harvard.med.screensaver.db.Criterion.Operator;
import edu.harvard.med.screensaver.db.GenericEntityDAO;
import edu.harvard.med.screensaver.db.Query;
import edu.harvard.med.screensaver.db.hqlbuilder.HqlBuilder;
import edu.harvard.med.screensaver.db.hqlbuilder.JoinType;
import edu.harvard.med.screensaver.model.AbstractEntity;
import edu.harvard.med.screensaver.model.meta.Cardinality;
import edu.harvard.med.screensaver.model.meta.PropertyNameAndValue;
import edu.harvard.med.screensaver.model.meta.PropertyPath;
import edu.harvard.med.screensaver.model.meta.RelationshipPath;
/**
* DataFetcher that fetches tuples from persistent storage. Each tuple property is specified via {@link PropertyPath},
* to be specified via {@link #setPropertiesToFetch}.
*/
public class TupleDataFetcher<E extends AbstractEntity,K> extends PropertyPathDataFetcher<Tuple<K>,E,K>
{
private static Logger log = Logger.getLogger(TupleDataFetcher.class);
private static final String COV_ALIAS_SUFFIX = "COV";
private PropertyPath<E> _idProperty;
public TupleDataFetcher(Class<E> rootEntityClass, GenericEntityDAO dao)
{
super(rootEntityClass, dao);
_idProperty = RelationshipPath.from(_rootEntityClass).toId();
}
@Override
public List<Tuple<K>> fetchAllData()
{
log.debug("fetching all data");
return Lists.newArrayList(doFetchData(Collections.<K>emptySet()).values());
}
@Override
public Map<K,Tuple<K>> fetchData(Set<K> keys)
{
if (log.isDebugEnabled()) {
log.debug("fetching data subset: " + keys);
}
Map<K,Tuple<K>> result = doFetchData(keys);
assert result.size() == keys.size() : "fetch data query result did not return all requested entities";
return result;
}
/**
* @param keys if null, fetches all entities for the root entity type (subject
* to normal column criteria)
*/
protected Map<K,Tuple<K>> doFetchData(Set<K> keys)
{
// collate properties into groups of PropertyPaths having same RelationshipPath;
// this will allow us to execute one query for each group of properties that are from the same entity type
Multimap<RelationshipPath<E>,PropertyPath<E>> pathGroups =
Multimaps.index(getProperties(), new Function<PropertyPath<E>,RelationshipPath<E>>() {
public RelationshipPath<E> apply(PropertyPath<E> p)
{
return p.getAncestryPath();
}
});
Map<K,Tuple<K>> tuples = Maps.newHashMapWithExpectedSize(keys.size());
for (Collection<PropertyPath<E>> propertyPaths : pathGroups.asMap().values()) {
List<PropertyPath<E>> orderedPropertyPaths = Lists.newArrayList(propertyPaths);
if (log.isDebugEnabled()) {
log.debug("fetching " + keys.size() + " values for properties " + orderedPropertyPaths);
}
List<Object[]> result = _dao.runQuery(buildQueryForProperty(orderedPropertyPaths, keys));
packageResultIntoTuples(tuples, orderedPropertyPaths, result);
}
return tuples;
}
private void packageResultIntoTuples(Map<K,Tuple<K>> tuples, List<PropertyPath<E>> orderedPropertyPaths, List<Object[]> result)
{
for (Object[] row : result) {
assert row.length == orderedPropertyPaths.size() + 1;
for (int i = 0; i < orderedPropertyPaths.size(); ++i) {
setTupleProperty(getOrCreateTuple(tuples, (K) row[0]), orderedPropertyPaths.get(i), row[i + 1]);
}
}
}
private void setTupleProperty(Tuple<K> tuple, PropertyPath<E> propertyPath, Object propertyValue)
{
if (propertyPath.getCardinality() == Cardinality.TO_MANY) {
tuple.addMultiPropertyElement(makePropertyKey(propertyPath), propertyValue);
}
else {
tuple.addProperty(makePropertyKey(propertyPath), propertyValue);
}
}
private Tuple<K> getOrCreateTuple(Map<K,Tuple<K>> tuples, K tupleKey)
{
if (!tuples.containsKey(tupleKey)) {
tuples.put(tupleKey, new Tuple<K>(tupleKey));
}
Tuple<K> tuple = tuples.get(tupleKey);
assert tuple != null;
return tuple;
}
public static String makePropertyKey(PropertyPath<?> propertyPath)
{
return propertyPath.toString().split("\\.", 2)[1];
}
private Query buildQueryForProperty(List<PropertyPath<E>> propertyPaths, Set<K> keys)
{
final HqlBuilder hql = new HqlBuilder();
Map<RelationshipPath<E>,String> path2Alias = Maps.newHashMap();
String rootEntityIdPropertyName = "id";
String propertyEntityAlias;
assert propertyPaths.size() >= 1;
RelationshipPath<E> relPath = propertyPaths.get(0).getAncestryPath();
// if possible, eliminate the root entity from the query, saving a join operation.
// this can only occur if the property to be retrieved is from an entity that is directly related to the root entity via a to-one relationship
if (!!!keys.isEmpty()) { // cannot apply this optimization if we're asked to fetch all data, since eliminating the root entity can break the expectations of addDomainRestrictions() implementations, which is called below
Iterator<String> inversePathIter = relPath.inversePathIterator();
if (inversePathIter.hasNext()) {
String inverseEntityName = inversePathIter.next();
if (inverseEntityName != null) {
// select tuple ID property from the second entity, rather than the root entity
path2Alias.put(relPath, getRootAlias());
assert relPath.entityClassIterator().hasNext();
hql.from(relPath.entityClassIterator().next(), getRootAlias());
rootEntityIdPropertyName = inverseEntityName + "." + rootEntityIdPropertyName;
// explicitly add restriction from rootEntity->relatedEntity, since this restriction would otherwise be lost
Iterator<PropertyNameAndValue> restrictionIterator = relPath.restrictionIterator();
PropertyNameAndValue restriction = restrictionIterator.hasNext() ? restrictionIterator.next() : null;
if (restriction != null) {
hql.where(getRootAlias(), restriction.getName(), Operator.EQUAL, restriction.getValue());
}
}
}
}
propertyEntityAlias = getOrCreateJoin(hql,
relPath,
path2Alias,
JoinType.LEFT);
hql.select(getRootAlias(), rootEntityIdPropertyName);
for (PropertyPath<E> propertyPath : propertyPaths) {
if (propertyPath.isCollectionOfValues()) {
// retrieve entire element as a tuple property
String covAlias = getOrCreateJoin(hql,
propertyPath,
path2Alias,
JoinType.LEFT);
hql.select(covAlias);
}
else if (propertyPath.getPropertyName().equals(PropertyPath.FULL_ENTITY)) {
// retrieve entire entity as a tuple property
hql.select(propertyEntityAlias);
}
else {
hql.select(propertyEntityAlias, propertyPath.getPropertyName());
}
}
if (!keys.isEmpty()) {
hql.whereIn(getRootAlias(), rootEntityIdPropertyName, keys);
}
else {
// if explicit set of keys has not been provided, we must still
// restrict result with top-level restrictions
addDomainRestrictions(hql);
}
if (log.isDebugEnabled()) {
log.debug("fetch data query for properties " + propertyPaths + ": " + hql);
}
return new Query() {
@Override
public List execute(Session session)
{
return hql.toQuery(session, true).list();
}
};
}
}