/*
* $Id$
*
* Copyright 2006-2014 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
/*------------------------------------------------------------------------------
*
* Written by: Josh Moore <josh.moore@gmx.de>
*
*------------------------------------------------------------------------------
*/
package ome.logic;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import ome.annotations.RolesAllowed;
import ome.api.IContainer;
import ome.api.IQuery;
import ome.api.ServiceInterface;
import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
import ome.model.ILink;
import ome.model.IObject;
import ome.model.containers.Dataset;
import ome.model.containers.Project;
import ome.model.core.Image;
import ome.model.fs.Fileset;
import ome.model.screen.Plate;
import ome.model.screen.Screen;
import ome.model.screen.Well;
import ome.parameters.Parameters;
import ome.services.query.PojosFindHierarchiesQueryDefinition;
import ome.services.query.PojosGetImagesByOptionsQueryDefinition;
import ome.services.query.PojosGetImagesQueryDefinition;
import ome.services.query.PojosGetUserImagesQueryDefinition;
import ome.services.query.PojosLoadHierarchyQueryDefinition;
import ome.services.query.Query;
import ome.tools.HierarchyTransformations;
import ome.tools.lsid.LsidUtils;
import ome.util.CBlock;
import ome.services.query.HierarchyNavigator;
import org.apache.commons.collections.CollectionUtils;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
/**
* implementation of the Pojos service interface.
*
* @author Josh Moore, <a href="mailto:josh.moore@gmx.de">josh.moore@gmx.de</a>
* @version 1.0 <small> (<b>Internal version:</b> $Rev$ $Date: 2007-10-03
* 13:25:20 +0100 (Wed, 03 Oct 2007) $) </small>
* @since OMERO 2.0
*/
@Transactional
public class PojosImpl extends AbstractLevel2Service implements IContainer {
/**
* Returns the Interface implemented by this class.
*
* @return See above.
*/
public final Class<? extends ServiceInterface> getServiceInterface() {
return IContainer.class;
}
// ~ READ
// =========================================================================
/** Query to load the number of annotations per image. */
final static String loadCountsImages = "select img from Image img "
+ "left outer join fetch img.annotationLinksCountPerOwner iac "
+ "where img in (:list)";
/** Query to load the number of annotations per dataset. */
final static String loadCountsDatasets = "select d from Dataset d "
+ "left outer join fetch d.annotationLinksCountPerOwner "
+ "left outer join fetch d.imageLinksCountPerOwner where d "
+ "in (:list)";
/** Query to load the number of annotations per plate. */
final static String loadCountsPlates = "select p from Plate p "
+ "left outer join fetch p.annotationLinksCountPerOwner "
+ "where p in (:list)";
/** Query to load the number of annotations per dataset. */
final static String loadLinksDatasets = "select d from Dataset d "
+ "left outer join fetch d.annotationLinksCountPerOwner "
+ "left outer join fetch d.imageLinksCountPerOwner where d "
+ "in (:list)";
/* A model object hierarchy navigator that is convenient for getImagesBySplitFilesets.
* To switch its API from bare Longs to an IObject-based query interface, it is easy to implement
* HierarchyNavigatorWrap<Class<? extends IObject>, IObject> and implement noteLookups with its methods. */
private static class HierarchyNavigatorPlain extends HierarchyNavigator {
HierarchyNavigatorPlain(IQuery iQuery) {
super(iQuery);
}
/**
* @{inheritDoc}
*/
public void prepareLookups(String toType, String fromType, Collection<Long> fromIds) {
super.prepareLookups(toType, fromType, fromIds);
}
/**
* @{inheritDoc}
*/
public ImmutableSet<Long> doLookup(String toType, String fromType, Long fromId) {
return super.doLookup(toType, fromType, fromId);
}
/**
* Perform {@link #prepareLookups(String, String, Collection)} and {@link #doLookup(String, String, Long)}
* for the given arguments and add the results to <code>toIdsAccumulator</code>.
* @param fromType the query objects' type, not <code>null</code>
* @param toType the type of the objects to which the query objects may be related, not <code>null</code>
* @param fromIds the query objects' database IDs, none <code>null</code>
* @param toIdsAccumulator collection into which to store the related objects' database IDs, not <code>null</code>
*/
public void noteLookups(String fromType, String toType, Collection<Long> fromIds, Collection<Long> toIdsAccumulator) {
super.prepareLookups(toType, fromType, fromIds);
for (final Long fromId : fromIds) {
toIdsAccumulator.addAll(super.doLookup(toType, fromType, fromId));
}
}
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
public Set loadContainerHierarchy(Class rootNodeType, Set rootNodeIds,
Parameters options) {
options = new Parameters(options); // Checks for null
if (null == rootNodeIds && !options.isExperimenter() && !options.isGroup()) {
throw new ApiUsageException(
"Set of ids for loadContainerHierarchy() may not be null "
+ "if experimenter and group options are null.");
}
if (!Project.class.equals(rootNodeType)
&& !Dataset.class.equals(rootNodeType)
&& !Screen.class.equals(rootNodeType) &&
!Plate.class.equals(rootNodeType)) {
throw new ApiUsageException(
"Class parameter for loadContainerIHierarchy() must be in "
+ "{Project,Dataset, Screen, Plate}, not "
+ rootNodeType);
}
Query<List<IObject>> q = getQueryFactory().lookup(
PojosLoadHierarchyQueryDefinition.class.getName(),
options.addClass(rootNodeType).addIds(rootNodeIds));
List<IObject> l = iQuery.execute(q);
Dataset d;
Plate plate;
// WORKAROUND ticket:882
if (Project.class.equals(rootNodeType)) {
Set<Dataset> datasets = new HashSet<Dataset>();
Project p;
for (IObject o : l) {
p = (Project) o;
datasets.addAll(p.linkedDatasetList());
}
if (options.isOrphan()) {
if (CollectionUtils.isEmpty(rootNodeIds)) {
Iterator<Dataset> i = datasets.iterator();
Set<Long> linked = new HashSet<Long>();
while (i.hasNext()) {
linked.add(i.next().getId());
}
q = getQueryFactory().lookup(
PojosLoadHierarchyQueryDefinition.class.getName(),
options.addClass(Dataset.class).addIds(rootNodeIds));
List<IObject> list = iQuery.execute(q);
Iterator<IObject> j = list.iterator();
Long id;
Map<Long, Dataset> notLinked = new HashMap<Long, Dataset>();
while (j.hasNext()) {
d = (Dataset) j.next();
id = d.getId();
if (!linked.contains(id)) {
notLinked.put(id, d);// not linked to user's project
}
}
StringBuffer sb = new StringBuffer();
sb.append("select this from Project this ");
sb.append("left outer join fetch this.datasetLinks pdl ");
sb.append("left outer join fetch pdl.child ds ");
sb.append("where ds in (:list)");
if (notLinked.size() > 0) {
List<Dataset> nl = new ArrayList<Dataset>();
nl.addAll(notLinked.values());
List<IObject> projects =
iQuery.findAllByQuery(sb.toString(),
new Parameters().addList("list", nl));
if (projects.isEmpty()) {
datasets.addAll(nl);
l.addAll(nl);
} else { //some datasets are in the projects
for (IObject o : projects) {
p = (Project) o;
List<Dataset> ll = p.linkedDatasetList();
for (Dataset data : ll) {
if (notLinked.containsKey(data.getId())) {
notLinked.remove(data.getId());
}
}
}
if (notLinked.size() > 0) {
nl = new ArrayList<Dataset>();
nl.addAll(notLinked.values());
datasets.addAll(nl);
l.addAll(nl);
}
}
}
}
}
if (datasets.size() > 0) {
iQuery.findAllByQuery(loadCountsDatasets, new Parameters()
.addSet("list", datasets));
}
} else if (Dataset.class.isAssignableFrom(rootNodeType)) {
Set<Image> images = new HashSet<Image>();
for (IObject o : l) {
d = (Dataset) o;
images.addAll(d.linkedImageList());
}
if (images.size() > 0) {
iQuery.findAllByQuery(loadCountsImages, new Parameters()
.addSet("list", images));
}
// WORKAROUND ticket:907
// Destructive changes in this block
if (!options.isLeaves()) {
EvictBlock<Dataset> evict = new EvictBlock<Dataset>();
for (IObject o : l) {
d = (Dataset) o;
evict.call(d);
d.putAt(Dataset.IMAGELINKS, null);
}
}
} else if (Screen.class.isAssignableFrom(rootNodeType)) {
Set<Plate> plates = new HashSet<Plate>();
Screen p;
for (IObject o : l) {
p = (Screen) o;
plates.addAll(p.linkedPlateList());
}
if (options.isOrphan()) {
if (CollectionUtils.isEmpty(rootNodeIds)) {
Iterator<Plate> i = plates.iterator();
Set<Long> linked = new HashSet<Long>();
while (i.hasNext()) {
linked.add(i.next().getId());
}
q = getQueryFactory().lookup(
PojosLoadHierarchyQueryDefinition.class.getName(),
options.addClass(Plate.class).addIds(rootNodeIds));
List<IObject> list = iQuery.execute(q);
Iterator<IObject> j = list.iterator();
Long id;
Plate pp;
Map<Long, Plate> notLinked = new HashMap<Long, Plate>();
while (j.hasNext()) {
pp = (Plate) j.next();
id = pp.getId();
if (!linked.contains(id)) {
notLinked.put(id, pp);// not linked to user's screen
}
}
StringBuffer sb = new StringBuffer();
sb.append("select this from Screen this ");
sb.append("left outer join fetch this.plateLinks pdl ");
sb.append("left outer join fetch pdl.child ds ");
sb.append("where ds in (:list)");
if (notLinked.size() > 0) {
List<Plate> nl = new ArrayList<Plate>();
nl.addAll(notLinked.values());
List<IObject> screens =
iQuery.findAllByQuery(sb.toString(),
new Parameters().addList("list", nl));
if (screens.isEmpty()) {
plates.addAll(nl);
l.addAll(nl);
} else { //some datasets are in the projects
for (IObject o : screens) {
p = (Screen) o;
List<Plate> ll = p.linkedPlateList();
for (Plate data : ll) {
if (notLinked.containsKey(data.getId())) {
notLinked.remove(data.getId());
}
}
}
if (notLinked.size() > 0) {
nl = new ArrayList<Plate>();
nl.addAll(notLinked.values());
plates.addAll(nl);
l.addAll(nl);
}
}
}
}
}
if (plates.size() > 0) {
iQuery.findAllByQuery(loadCountsPlates, new Parameters()
.addSet("list", plates));
}
}
return new HashSet<IObject>(l);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
public Set findContainerHierarchies(final Class rootNodeType,
final Set imageIds, Parameters options) {
options = new Parameters(options); // Checks for null
// TODO refactor to use Hierarchy class H.isTopLevel()
if (!(Project.class.equals(rootNodeType))) {
throw new ApiUsageException(
"Class parameter for findContainerHierarchies() must be"
+ " in {Project}, not "
+ rootNodeType);
}
Query<List<Image>> q = getQueryFactory().lookup(
PojosFindHierarchiesQueryDefinition.class.getName(),
options.addClass(rootNodeType).addIds(imageIds));
List<Image> l = iQuery.execute(q);
//
// Destructive changes below this point.
//
// TODO; this if-else statement could be removed if Transformations
// did their own dispatching
// TODO: logging, null checking. daos should never return null
// TODO then size!
if (Project.class.equals(rootNodeType)) {
if (imageIds.size() == 0) {
return new HashSet();
}
return HierarchyTransformations.invertPDI(new HashSet<Image>(l),
new EvictBlock<IObject>());
}
else {
throw new InternalException("This can't be reached.");
}
}
static final Map<Class, String> paginationQueries = new HashMap();
static {
paginationQueries.put(Dataset.class,
"select link.child.id from DatasetImageLink "
+ " link where link.parent.id in (:ids)"
+ "order by link.child.id");
paginationQueries
.put(
Project.class,
"select distinct dil.child.id from ProjectDatasetLink pdl "
+ "join pdl.child ds join ds.imageLinks as dil "
+ "where pdl.parent.id in (:ids) order by dil.child.id");
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public Set getImages(final Class rootNodeType, final Set rootNodeIds,
Parameters options) {
if (rootNodeIds.size() == 0) {
return new HashSet();
}
options = new Parameters(options); // Checks for null
final Parameters view = options;
// Effective values
Class effType = rootNodeType;
Set<Long> effIds = rootNodeIds;
if (options.isPagination()) {
final String query = paginationQueries.get(rootNodeType);
if (query == null) {
throw new ApiUsageException(rootNodeType.getName()
+ " does not support pagination yet.");
}
effType = Image.class;
effIds = new HashSet<Long>((List<Long>) iQuery
.execute(new HibernateCallback() {
public Object doInHibernate(Session s)
throws HibernateException, SQLException {
org.hibernate.Query q = s.createQuery(query);
q.setParameterList("ids", rootNodeIds);
// ticket:1232
if (view.getLimit() != null) {
q.setMaxResults(view.getLimit());
} else {
q.setMaxResults(Integer.MAX_VALUE);
}
if (view.getOffset() != null) {
q.setFirstResult(view.getOffset());
} else {
q.setFirstResult(0);
}
return q.list();
}
}));
if (effIds == null || effIds.size() == 0) {
return new HashSet();
}
/* paging has now been done */
options = options.page(0, Integer.MAX_VALUE);
}
Query<List<IObject>> q = getQueryFactory().lookup(
PojosGetImagesQueryDefinition.class.getName(),
options.addIds(effIds).addClass(effType));
List<IObject> l = iQuery.execute(q);
return new HashSet<IObject>(l);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public Set getImagesByOptions(Parameters options) {
options = new Parameters(options); // Checks for null
if (options.getStartTime() == null && options.getEndTime() == null) {
throw new ApiUsageException("start or end time option "
+ "is required for getImagesByOptions().");
}
Query<List<IObject>> q = getQueryFactory().lookup(
PojosGetImagesByOptionsQueryDefinition.class.getName(),
options);
List<IObject> l = iQuery.execute(q);
return new HashSet<IObject>(l);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
public Map<Long, Map<Boolean, List<Long>>> getImagesBySplitFilesets(Map<Class<? extends IObject>, List<Long>> included,
Parameters options) {
/* note which entities have been explicitly referenced */
final Set<Long> projectIds = new HashSet<Long>();
final Set<Long> datasetIds = new HashSet<Long>();
final Set<Long> screenIds = new HashSet<Long>();
final Set<Long> plateIds = new HashSet<Long>();
final Set<Long> wellIds = new HashSet<Long>();
final Set<Long> filesetIds = new HashSet<Long>();
final Set<Long> imageIds = new HashSet<Long>();
for (final Entry<Class<? extends IObject>, List<Long>> typeAndIds : included.entrySet()) {
final Class<? extends IObject> type = typeAndIds.getKey();
final List<Long> ids = typeAndIds.getValue();
if (Project.class.isAssignableFrom(type)) {
projectIds.addAll(ids);
} else if (Dataset.class.isAssignableFrom(type)) {
datasetIds.addAll(ids);
} else if (Screen.class.isAssignableFrom(type)) {
screenIds.addAll(ids);
} else if (Plate.class.isAssignableFrom(type)) {
plateIds.addAll(ids);
} else if (Well.class.isAssignableFrom(type)) {
wellIds.addAll(ids);
} else if (Image.class.isAssignableFrom(type)) {
imageIds.addAll(ids);
} else if (Fileset.class.isAssignableFrom(type)) {
filesetIds.addAll(ids);
}
}
/* also note which entities have been implicitly referenced */
final HierarchyNavigatorPlain hierarchyNavigator = new HierarchyNavigatorPlain(iQuery);
hierarchyNavigator.noteLookups("/Project", "/Dataset", projectIds, datasetIds);
hierarchyNavigator.noteLookups("/Dataset", "/Image", datasetIds, imageIds);
hierarchyNavigator.noteLookups("/Screen", "/Plate", screenIds, plateIds);
hierarchyNavigator.noteLookups("/Plate", "/Well", plateIds, wellIds);
hierarchyNavigator.noteLookups("/Well", "/Image", wellIds, imageIds);
hierarchyNavigator.noteLookups("/Fileset", "/Image", filesetIds, imageIds);
/* note which filesets are associated with referenced images */
final Set<Long> filesetIdsRequired = new HashSet<Long>();
hierarchyNavigator.noteLookups("/Image", "/Fileset", imageIds, filesetIdsRequired);
/* make sure that associated filesets have all their images referenced */
final Map<Long, Map<Boolean, List<Long>>> imagesBySplitFilesets = new HashMap<Long, Map<Boolean, List<Long>>>();
final Set<Long> filesetIdsMissing = Sets.difference(filesetIdsRequired, filesetIds);
hierarchyNavigator.prepareLookups("/Image", "/Fileset", filesetIdsMissing);
for (final long filesetIdMissing : filesetIdsMissing) {
final Set<Long> imageIdsRequiredUnordered = hierarchyNavigator.doLookup("/Image", "/Fileset", filesetIdMissing);
final SortedSet<Long> imageIdsRequired = new TreeSet<Long>(imageIdsRequiredUnordered);
final Set<Long> includedImageIds = Sets.intersection(imageIdsRequired, imageIds);
final Set<Long> excludedImageIds = Sets.difference(imageIdsRequired, includedImageIds);
if (!excludedImageIds.isEmpty()) {
final Map<Boolean, List<Long>> partitionedImages = new HashMap<Boolean, List<Long>>(2);
partitionedImages.put(true, new ArrayList<Long>(includedImageIds));
partitionedImages.put(false, new ArrayList<Long>(excludedImageIds));
imagesBySplitFilesets.put(filesetIdMissing, partitionedImages);
}
}
return imagesBySplitFilesets;
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public Set getUserImages(Parameters options) {
options = new Parameters(options); // Checks for null
if (!options.isExperimenter() && !options.isGroup()) {
throw new ApiUsageException("experimenter or group option "
+ "is required for getUserImages().");
}
Query<List<Image>> q = getQueryFactory().lookup(
PojosGetUserImagesQueryDefinition.class.getName(), options);
List<Image> l = iQuery.execute(q);
return new HashSet<Image>(l);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
public Map getCollectionCount(String type, String property, Set ids,
Parameters options) {
String parsedProperty = LsidUtils.parseField(property);
checkType(type);
checkProperty(type, parsedProperty);
Map<Long, Integer> results = new HashMap<Long, Integer>();
String query = "select size(table." + parsedProperty + ") from " + type
+ " table where table.id = :id";
// FIXME: optimize by doing new list(id,size(table.property)) ... group
// by id
for (Iterator iter = ids.iterator(); iter.hasNext();) {
Long id = (Long) iter.next();
Query<List<Integer>> q = getQueryFactory().lookup(query,
new Parameters().addId(id));
Integer count = iQuery.execute(q).get(0);
results.put(id, count);
}
return results;
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = true)
public Collection retrieveCollection(IObject arg0, String arg1, Parameters arg2) {
IObject context = iQuery.get(arg0.getClass(), arg0.getId());
Collection c = (Collection) context.retrieve(arg1); // FIXME not
// type.o.null safe
iQuery.initialize(c);
return c;
}
// ~ WRITE
// =========================================================================
@Override
@RolesAllowed("user")
@Transactional(readOnly = false)
public IObject createDataObject(IObject arg0, Parameters arg1) {
return iUpdate.saveAndReturnObject(arg0);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = false)
public IObject[] createDataObjects(IObject[] arg0, Parameters arg1) {
return iUpdate.saveAndReturnArray(arg0);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = false)
public void unlink(ILink[] arg0, Parameters arg1) {
deleteDataObjects(arg0, arg1);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = false)
public ILink[] link(ILink[] arg0, Parameters arg1) {
IObject[] retVal = iUpdate.saveAndReturnArray(arg0);
// IUpdate returns an IObject array here. Can't be cast using (Link[])
ILink[] links = new ILink[retVal.length];
System.arraycopy(retVal, 0, links, 0, retVal.length);
return links;
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = false)
public IObject updateDataObject(IObject arg0, Parameters arg1) {
return iUpdate.saveAndReturnObject(arg0);
}
@Override
@RolesAllowed("user")
@Transactional(readOnly = false)
public IObject[] updateDataObjects(IObject[] arg0, Parameters arg1) {
return iUpdate.saveAndReturnArray(arg0);
}
/**
* Delete a data object.
* @param row the object to delete
* @param options the parameters to apply (ignored)
*/
private void deleteDataObject(IObject row, Parameters options) {
iUpdate.deleteObject(row);
}
/**
* Delete some data objects.
* @param rows the objects to delete
* @param options the parameters to apply (ignored)
*/
private void deleteDataObjects(IObject[] rows, Parameters options) {
for (IObject object : rows) {
deleteDataObject(object, options);
}
}
// ~ Helpers
// =========================================================================
final static String alphaNumeric = "^\\w+$";
final static String alphaNumericDotted = "^\\w[.\\w]+$"; // TODO
// annotations
protected void checkType(String type) {
if (!type.matches(alphaNumericDotted)) {
throw new ApiUsageException(
"Type argument to getCollectionCount may ONLY be "
+ "alpha-numeric with dots (" + alphaNumericDotted
+ ")");
}
if (!iQuery.checkType(type)) {
throw new ApiUsageException(type + " is an unknown type.");
}
}
protected void checkProperty(String type, String property) {
if (!property.matches(alphaNumeric)) {
throw new ApiUsageException("Property argument to "
+ "getCollectionCount may ONLY be alpha-numeric ("
+ alphaNumeric + ")");
}
if (!iQuery.checkProperty(type, property)) {
throw new ApiUsageException(type + "." + property
+ " is an unknown property on type " + type);
}
}
@SuppressWarnings("unchecked")
class EvictBlock<E extends IObject> implements CBlock {
public E call(IObject object) {
iQuery.evict(object);
return (E) object;
};
}
}