/*
* Copyright (C) 2013-2014 University of Dundee & Open Microscopy Environment.
* All rights reserved.
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package ome.services.query;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import ome.api.IQuery;
import ome.parameters.Parameters;
/**
* Query the database for relationships between model objects.
* Caches results, so designed for a short lifetime.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.0
*/
public class HierarchyNavigator {
/* This class and {@link HierarchyWrap} are designed to make it easy to adjust the Java types
* via which the model object hierarchy is navigated, and to make the HQL queries efficient
* (batching, caching), at the small expense of constructing instances of simple Java objects.
* The methods are not public to avoid polluting users of subclasses of {@link HierarchyWrap}.
*/
/** HQL queries to map from ID of first target type to that of the second */
private static final ImmutableMap<Map.Entry<String, String>, String> hqlFromTo;
static {
/* note that there is not yet any treatment of /PlateAcquisition or /WellSample */
final Builder<Map.Entry<String, String>, String> builder = ImmutableMap.builder();
builder.put(Maps.immutableEntry("/Project", "/Dataset"),
"SELECT parent.id, child.id FROM ProjectDatasetLink WHERE parent.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Dataset", "/Image"),
"SELECT parent.id, child.id FROM DatasetImageLink WHERE parent.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Screen", "/Plate"),
"SELECT parent.id, child.id FROM ScreenPlateLink WHERE parent.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Plate", "/Well"),
"SELECT plate.id, id FROM Well WHERE plate.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Well", "/Image"),
"SELECT well.id, image.id FROM WellSample WHERE well.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Fileset", "/Image"),
"SELECT fileset.id, id FROM Image WHERE fileset.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Image", "/Fileset"),
"SELECT id, fileset.id FROM Image WHERE fileset.id IS NOT NULL AND id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Image", "/Well"),
"SELECT image.id, well.id FROM WellSample WHERE image.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Well", "/Plate"),
"SELECT id, plate.id FROM Well WHERE id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Plate", "/Screen"),
"SELECT child.id, parent.id FROM ScreenPlateLink WHERE child.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Image", "/Dataset"),
"SELECT child.id, parent.id FROM DatasetImageLink WHERE child.id IN (:" + Parameters.IDS + ")");
builder.put(Maps.immutableEntry("/Dataset", "/Project"),
"SELECT child.id, parent.id FROM ProjectDatasetLink WHERE child.id IN (:" + Parameters.IDS + ")");
hqlFromTo = builder.build();
}
/** available query service */
protected final IQuery iQuery;
/** cache of query results */
private final ModelObjectCache cache = new ModelObjectCache();
/**
* Construct a new hierarchy navigator.
* @param iQuery the query service
*/
protected HierarchyNavigator(IQuery iQuery) {
this.iQuery = iQuery;
}
/**
* Perform the database query to discover the IDs of the related objects.
* @param toType the type of the objects to which the query object may be related, not <code>null</code>
* @param fromType the query object's type, not <code>null</code>
* @param fromIds the query objects' database IDs, none <code>null</code>
* @return pairs of database IDs: of the query object, and an object to which it relates
*/
private List<Object[]> doQuery(String toType, String fromType, Collection<Long> fromIds) {
final String queryString = hqlFromTo.get(Maps.immutableEntry(fromType, toType));
if (queryString == null) {
throw new IllegalArgumentException("not implemented for " + fromType + " to " + toType);
}
return this.iQuery.projection(queryString, new Parameters().addIds(fromIds));
}
/**
* Batch bulk database queries to prime the cache for {@link #doLookup(String, String, Long)}.
* It is not necessary to call this method, but it is advised if many lookups are anticipated.
* @param toType the type of the objects to which the query objects may be related, not <code>null</code>
* @param fromType the query object's type, not <code>null</code>
* @param fromIds the query objects' database IDs, none <code>null</code>
*/
protected void prepareLookups(String toType, String fromType, Collection<Long> fromIds) {
/* note which query object IDs have not already had results cached */
final Set<Long> fromIdsToQuery = new HashSet<Long>(fromIds);
for (final long fromId : fromIds) {
if (cache.getFromCache(fromType, fromId, toType) != null) {
fromIdsToQuery.remove(fromId);
}
}
if (fromIdsToQuery.isEmpty()) {
/* ... all of them are already cached */
return;
}
/* collate the results from multiple batches */
final SetMultimap<Long, Long> fromIdsToIds = HashMultimap.create();
for (final List<Long> fromIdsToQueryBatch : Iterables.partition(fromIdsToQuery, 256)) {
for (final Object[] queryResult : doQuery(toType, fromType, fromIdsToQueryBatch)) {
fromIdsToIds.put((Long) queryResult[0], (Long) queryResult[1]);
}
}
/* cache the results by query object */
for (final Entry<Long, Collection<Long>> fromIdToIds : fromIdsToIds.asMap().entrySet()) {
cache.putIntoCache(fromType, fromIdToIds.getKey(), toType, ImmutableSet.copyOf(fromIdToIds.getValue()));
}
/* note empty results so that the database is not again queried */
for (final Long fromId : Sets.difference(fromIdsToQuery, fromIdsToIds.keySet())) {
cache.putIntoCache(fromType, fromId, toType, ImmutableSet.<Long>of());
}
}
/**
* Look up which objects of a given type relate to the given query object.
* Caches results, and one may bulk-cache results in advance using {@link #prepareLookups(String, String, Collection)}.
* @param toType the type of the objects to which the query object may be related, not <code>null</code>
* @param fromType the query object's type, not <code>null</code>
* @param fromId the query object's database ID, not <code>null</code>
* @return the related objects' database IDs, never <code>null</code>
*/
protected ImmutableSet<Long> doLookup(String toType, String fromType, Long fromId) {
final ImmutableSet<Long> result = cache.getFromCache(fromType, fromId, toType);
if (result == null) {
/* cache miss, so query the single object */
final ImmutableSet.Builder<Long> toIdsBuilder = ImmutableSet.builder();
for (final Object[] queryResult : doQuery(toType, fromType, Collections.singleton(fromId))) {
toIdsBuilder.add((Long) queryResult[1]);
}
final ImmutableSet<Long> toIds = toIdsBuilder.build();
cache.putIntoCache(fromType, fromId, toType, toIds);
return toIds;
} else {
/* cache hit */
return result;
}
}
}