/*
* $Id$
*
* Copyright 2008 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.blitz.impl;
import static omero.rtypes.rint;
import static omero.rtypes.rlong;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import ome.api.IShare;
import ome.conditions.InternalException;
import ome.conditions.ValidationException;
import ome.model.IObject;
import ome.model.annotations.Annotation;
import ome.model.annotations.SessionAnnotationLink;
import ome.model.core.Image;
import ome.model.meta.Event;
import ome.model.meta.EventLog;
import ome.security.AdminAction;
import ome.security.SecuritySystem;
import ome.services.blitz.util.BlitzExecutor;
import ome.services.blitz.util.BlitzOnly;
import ome.services.blitz.util.ServiceFactoryAware;
import ome.services.sessions.SessionManager;
import ome.services.throttling.Adapter;
import ome.services.util.Executor.SimpleWork;
import ome.system.Principal;
import ome.system.ServiceFactory;
import ome.tools.hibernate.QueryBuilder;
import omero.ApiUsageException;
import omero.RTime;
import omero.ServerError;
import omero.api.AMD_ITimeline_countByPeriod;
import omero.api.AMD_ITimeline_getByPeriod;
import omero.api.AMD_ITimeline_getEventLogsByPeriod;
import omero.api.AMD_ITimeline_getMostRecentAnnotationLinks;
import omero.api.AMD_ITimeline_getMostRecentObjects;
import omero.api.AMD_ITimeline_getMostRecentShareCommentLinks;
import omero.api._ITimelineOperations;
import omero.sys.Filter;
import omero.sys.Parameters;
import omero.util.IceMapper;
import org.hibernate.Query;
import org.hibernate.Session;
import org.springframework.transaction.annotation.Transactional;
import Ice.Current;
/**
* implementation of the ITimeline service interface.
*
* @since Beta4
*/
public class TimelineI extends AbstractAmdServant implements
_ITimelineOperations, ServiceFactoryAware, BlitzOnly {
protected ServiceFactoryI factory;
protected SessionManager sm;
protected SecuritySystem ss;
public TimelineI(BlitzExecutor be) {
super(null, be);
}
public void setSessionManager(SessionManager sm) {
this.sm = sm;
}
public void setSecuritySystem(SecuritySystem ss) {
this.ss = ss;
}
public void setServiceFactory(ServiceFactoryI sf) {
this.factory = sf;
}
// ~ Service methods
// =========================================================================
static final String LOOKUP_SHARE_COMMENTS = "select l from SessionAnnotationLink l "
+ "join fetch l.details.owner "
+ "join fetch l.parent as share "
+ "join fetch l.child as comment "
+ "join fetch comment.details.owner "
+ "join fetch comment.details.creationEvent "
+ "where comment.details.owner.id !=:id "
+ "and share.id in (:ids) "
+ "order by comment.details.creationEvent.time desc";
// TODO fix mutability
static final List<String> ALLTYPES = Arrays.asList("RenderingDef", "Image",
"Project", "Dataset", "Annotation");
static final Map<String, String> ORDERBY = new HashMap<String, String>();
static final Map<String, String> BYPERIOD = new HashMap<String, String>();
static final Map<String, String> OWNERSHIP = new HashMap<String, String>();
static {
String WHERE_OBJ_DETAILS = "where"
+ " (obj.details.creationEvent.time >= :start "
+ " or obj.details.updateEvent.time >= :start) "
+ "and (obj.details.creationEvent.time <= :end"
+ " or obj.details.updateEvent.time <= :end ) ";
BYPERIOD.put("Project", "from Project obj "
+ "join @FETCH@ obj.details.creationEvent "
+ "join @FETCH@ obj.details.owner "
+ "join @FETCH@ obj.details.group " + WHERE_OBJ_DETAILS);
OWNERSHIP.put("Project", "obj");
ORDERBY.put("Project", "order by obj.details.updateEvent.id desc");
BYPERIOD.put("Dataset", "from Dataset obj "
+ "join @FETCH@ obj.details.creationEvent "
+ "join @FETCH@ obj.details.owner "
+ "join @FETCH@ obj.details.group "
//+ "left outer join @FETCH@ obj.projectLinks pdl "
//+ "left outer join @FETCH@ pdl.parent p "
+ WHERE_OBJ_DETAILS);
OWNERSHIP.put("Dataset", "obj");
ORDERBY.put("Dataset", "order by obj.details.updateEvent.id desc");
BYPERIOD.put("RenderingDef", "from RenderingDef obj join @FETCH@ "
+ "obj.details.creationEvent join @FETCH@ obj.details.owner "
+ "join @FETCH@ obj.details.group left outer join @FETCH@ "
+ "obj.pixels p left outer join @FETCH@ p.image i "
+ WHERE_OBJ_DETAILS);
OWNERSHIP.put("RenderingDef", "i");
ORDERBY.put("RenderingDef",
"order by i.details.creationEvent.time desc");
BYPERIOD.put("Image", "from Image obj "
+ "join @FETCH@ obj.details.creationEvent "
+ "join @FETCH@ obj.details.owner "
+ "join @FETCH@ obj.details.group "
//+ "left outer join @FETCH@ obj.datasetLinks dil "
//+ "left outer join @FETCH@ dil.parent d "
//+ "left outer join @FETCH@ d.projectLinks pdl "
//+ "left outer join @FETCH@ pdl.parent p "
+ "where "
+ " obj.acquisitionDate >= :start "
+ "and obj.acquisitionDate <= :end ");
OWNERSHIP.put("Image", "obj");
ORDERBY.put("Image", "order by obj.acquisitionDate desc");
BYPERIOD.put("EventLog", "from EventLog obj "
+ "left outer join @FETCH@ obj.event ev where "
+ " obj.entityType in ("
+ " 'ome.model.containers.Dataset', "
+ " 'ome.model.containers.Project') "
+ " and obj.action in ( 'INSERT', 'UPDATE', 'REINDEX') "
+ " and ev.id in ( "
+ " select e.id from Event e where "
+ " e.time >= :start and e.time <= :end ");
// NOTE This query requires special handling in do_periodQuery
// to properly handle the ownership via Event and closing the
// subquery.
}
public void countByPeriod_async(final AMD_ITimeline_countByPeriod __cb,
final List<String> types, final RTime start, final RTime end,
final Parameters p, final Current __current) throws ServerError {
final IceMapper mapper = new IceMapper(IceMapper.PRIMITIVE_MAP);
runnableCall(__current, new Adapter(__cb, __current, mapper, factory
.getExecutor(), factory.principal, new SimpleWork(this,
"countByPeriod") {
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
Parameters pWithParameters = applyDefaults(p);
return do_periodQuery(true, types, start, end, Long
.valueOf(-1L), session, pWithParameters);
}
}));
}
public void getByPeriod_async(final AMD_ITimeline_getByPeriod __cb,
final List<String> types, final RTime start, final RTime end,
final omero.sys.Parameters p, final boolean merge, Current __current)
throws ServerError {
final IceMapper mapper = new IceMapper(
IceMapper.PRIMITIVE_FILTERABLE_COLLECTION_MAP);
runnableCall(__current, new Adapter(__cb, __current, mapper, factory
.getExecutor(), factory.principal, new SimpleWork(this,
"getByPeriod") {
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
Parameters pWithDefaults = applyDefaults(p);
Map<String, List<IObject>> returnValue = (Map<String, List<IObject>>) do_periodQuery(
false, types, start, end, null, session, pWithDefaults);
if (merge) {
returnValue = mergeMap(returnValue, pWithDefaults, false);
}
return returnValue;
}
}));
}
public void getEventLogsByPeriod_async(
final AMD_ITimeline_getEventLogsByPeriod __cb, final RTime start,
final RTime end, final omero.sys.Parameters p,
final Current __current) throws ServerError {
final IceMapper mapper = new IceMapper(IceMapper.FILTERABLE_COLLECTION);
runnableCall(__current, new Adapter(__cb, __current, mapper, factory
.getExecutor(), factory.principal, new SimpleWork(this,
"getEventLogsByPeriod") {
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
Parameters pWithDefaults = applyDefaults(p);
Map<String, List<EventLog>> events = (Map<String, List<EventLog>>) do_periodQuery(
false, Arrays.asList("EventLog"), start, end, null,
session, pWithDefaults);
List<EventLog> logs = events.get("EventLog");
// WORKAROUND - currently there are no events for
// Image.acquisitionDate meaning we have to generate them
// here.
QueryBuilder qb = new QueryBuilder(256);
qb.select("i");
qb.from("Image", "i");
qb.join("i.details.owner", "owner", true, true);
qb.join("i.details.group", "group", true, true);
qb.where();
qb.and("i.acquisitionDate > :start ");
qb.param("start", new Timestamp(start.getValue()));
qb.and("i.acquisitionDate < :end ");
qb.param("end", new Timestamp(end.getValue()));
// OWNER/GROUP
applyOwnerGroup(pWithDefaults, qb, "owner.id", "group.id");
Query q = qb.query(session);
applyParameters(pWithDefaults, q);
List<Image> images = (List<Image>) q.list();
for (Image image : images) {
EventLog el = new EventLog();
el.setEntityId(image.getId());
el.setEntityType(image.getClass().getName());
el.setAction("INSERT");
el.setEvent(new Event());
el.getEvent().setTime(image.getAcquisitionDate());
logs.add(el);
}
return logs;
}
}));
}
public void getMostRecentObjects_async(
final AMD_ITimeline_getMostRecentObjects __cb,
final List<String> types, final omero.sys.Parameters p,
final boolean merge, Current __current) throws ServerError {
final IceMapper mapper = new IceMapper(
IceMapper.PRIMITIVE_FILTERABLE_COLLECTION_MAP);
runnableCall(__current, new Adapter(__cb, __current, mapper, factory
.getExecutor(), factory.principal, new SimpleWork(this,
"getMostRecentObjects") {
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
Parameters pWithDefaults = applyDefaults(p);
Map<String, List<IObject>> returnValue = (Map<String, List<IObject>>) do_periodQuery(
false, types, null, null, null, session, pWithDefaults);
if (merge) {
returnValue = mergeMap(returnValue, pWithDefaults, false);
}
return returnValue;
}
}));
}
public void getMostRecentAnnotationLinks_async(
final AMD_ITimeline_getMostRecentAnnotationLinks __cb,
final List<String> parentTypes, final List<String> childTypes,
final List<String> namespaces, final Parameters p,
final Current __current) throws ServerError {
final IceMapper mapper = new IceMapper(IceMapper.FILTERABLE_COLLECTION);
runnableCall(__current, new Adapter(__cb, __current, mapper, factory
.getExecutor(), factory.principal, new SimpleWork(this,
"getMostRecentAnnotationLinks") {
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
Parameters pWithDefaults = applyDefaults(p);
Map<String, List<IObject>> rv = new HashMap<String, List<IObject>>();
List<String> _parentTypes;
if (parentTypes == null || parentTypes.size() == 0) {
_parentTypes = Arrays.asList("Project", "Dataset", "Image");
} else {
_parentTypes = parentTypes;
}
for (String _parentType : _parentTypes) {
QueryBuilder qb = new QueryBuilder();
qb.select("link");
qb.from(_parentType + "AnnotationLink", "link");
qb.join("link.parent", "parent", false, false);
qb.join("link.child", "child", false, true);
qb.join("link.details.creationEvent", "creation", false,
true);
qb.join("link.details.updateEvent", "update", false, true);
qb.where();
qb.and("TRUE = TRUE ");
if (childTypes != null && childTypes.size() > 0) {
// WORKAROUND
if (childTypes.size() > 1) {
throw new ome.conditions.ApiUsageException(
"HHH-879: "
+ "You can only restrict to a "
+ "single annotation type at the moment");
}
for (String _childType : childTypes) {
try {
Class kls = mapper.omeroClass(_childType, true);
qb.and("child.class = " + kls.getName());
} catch (Exception e) {
throw new ValidationException("Error mapping: "
+ _childType);
}
}
}
// NAMESPACES
if (namespaces != null && namespaces.size() > 0) {
qb.and(" ( ");
String param = qb.unique_alias("ns");
qb.append("child.ns like :" + param);
qb.param(param, namespaces.get(0));
for (int i = 1; i < namespaces.size(); i++) {
qb.append(" OR ");
String param2 = qb.unique_alias("ns");
qb.append("child.ns like :" + param2);
qb.param(param2, namespaces.get(i));
}
qb.append(" ) ");
}
// OWNER/GROUP
applyOwnerGroup(p, qb, "link.details.owner.id",
"link.details.group.id");
// ORDER
qb.order("link.details.updateEvent.id", false);
Query q = qb.query(session);
applyParameters(p, q);
rv.put(_parentType, q.list());
}
return mergeList(rv, pWithDefaults, false);
}
}));
}
public void getMostRecentShareCommentLinks_async(
AMD_ITimeline_getMostRecentShareCommentLinks __cb,
final Parameters p, Current __current) throws ServerError {
final IceMapper mapper = new IceMapper(IceMapper.FILTERABLE_COLLECTION);
runnableCall(__current, new Adapter(__cb, __current, mapper, factory
.getExecutor(), factory.principal, new SimpleWork(this,
"getMostRecentShareComments") {
@Transactional(readOnly = true)
public Object doWork(org.hibernate.Session session,
final ServiceFactory sf) {
IShare sh = sf.getShareService();
Set<ome.model.meta.Session> shares = new HashSet<ome.model.meta.Session>();
Set<ome.model.meta.Session> shares1 = sh.getOwnShares(false);
Set<ome.model.meta.Session> shares2 = sh.getMemberShares(false);
if (shares1 != null) {
shares.addAll(shares1);
}
if (shares2 != null) {
shares.addAll(shares2);
}
if (shares.size() == 0) {
return new ArrayList<Annotation>(); // EARLY EXIT
}
final long userId = defaultId();
final Set<Long> ids = new HashSet<Long>();
for (ome.model.meta.Session s : shares) {
ids.add(s.getId());
}
final Parameters pWithDefaults = applyDefaults(p);
final ome.parameters.Parameters ome_p;
try {
ome_p = mapper.convert(pWithDefaults);
} catch (ApiUsageException e) {
throw new InternalException("Failed to convert parameters"
+ e.getMessage());
}
ome_p.addId(userId);
ome_p.addIds(ids);
final List<SessionAnnotationLink> rv = new ArrayList<SessionAnnotationLink>();
ss.runAsAdmin(new AdminAction() {
public void runAsAdmin() {
List<SessionAnnotationLink> links = sf
.getQueryService().findAllByQuery(
LOOKUP_SHARE_COMMENTS, ome_p);
rv.addAll(links);
}
});
return rv;
}
}));
}
// Helpers
// =========================================================================
/**
* Main implementation for most of the interface methods in TimelineI.
* Arguments: - parameters : if null, then no setFirst or setMax will be
* called on query
*/
private Map<?, ?> do_periodQuery(final boolean count,
final List<String> types, final RTime start, final RTime end,
final Object missingValue, final Session _s,
final Parameters parameters) {
final long activeStart;
if (start != null) {
activeStart = start.getValue();
} else {
activeStart = omero.rtypes.rtime_min().getValue();
}
final long activeEnd;
if (end != null) {
activeEnd = end.getValue();
} else {
activeEnd = omero.rtypes.rtime_max().getValue();
}
List<String> activeTypes = types;
if (types == null || types.size() == 0) {
activeTypes = new ArrayList<String>(ALLTYPES);
}
final Map<String, Object> returnValue = new HashMap<String, Object>();
for (final String type : activeTypes) {
String qString = BYPERIOD.get(type);
if (qString == null) {
returnValue.put(type, missingValue);
continue;
}
QueryBuilder qb = new QueryBuilder(256);
if (count) {
qb.select("count(obj)");
qb.skipFrom();
qb.append(qString.replaceAll("@FETCH@", ""));
} else {
qb.select("obj");
qb.skipFrom();
qb.append(qString.replaceAll("@FETCH@", "fetch"));
}
qb.skipWhere();
qb.and("");
String owningObject = OWNERSHIP.get(type);
if (owningObject == null) {
if ("EventLog".equals(type)) {
// SPECIAL LOGIC WORKAROUND for the complicated EventLog
// query
if (parameters != null && parameters.theFilter != null) {
if (parameters.theFilter.ownerId != null) {
qb.and("e.experimenter.id = :owner_id");
qb.param("owner_id", parameters.theFilter.ownerId
.getValue());
}
if (parameters.theFilter.groupId != null) {
qb.and("e.experimenterGroup.id = :group_id");
qb.param("group_id", parameters.theFilter.groupId
.getValue());
}
}
qb.append(")");
} else {
throw new InternalException("No ownership info for: "
+ type);
}
} else {
applyOwnerGroup(parameters, qb, owningObject
+ ".details.owner.id", owningObject
+ ".details.group.id");
}
if (!count) {
String orderBy = ORDERBY.get(type);
if (orderBy != null) {
qb.append(orderBy);
}
}
qb.param("start", new Timestamp(activeStart));
qb.param("end", new Timestamp(activeEnd));
Query q = qb.query(_s);
applyParameters(parameters, q);
if (count) {
returnValue.put(type, q.uniqueResult());
} else {
returnValue.put(type, q.list());
}
}
return returnValue;
}
/**
* @see ticket:1232
*/
private void applyParameters(final Parameters parameters, Query q) {
int limit = Integer.MAX_VALUE;
int offset = 0;
if (parameters != null && parameters.theFilter != null) {
Filter f = parameters.theFilter;
if (f.offset != null) {
offset = f.offset.getValue();
}
if (f.limit != null) {
limit = f.limit.getValue();
}
}
q.setFirstResult(offset);
q.setMaxResults(limit);
}
private long defaultId() {
String session = this.factory.sessionId().name;
return sm.getEventContext(new Principal(session)).getCurrentUserId();
}
static class Entry {
final Timestamp update;
final String key;
final IObject obj;
Entry(String key, IObject obj) {
this.key = key;
this.obj = obj;
this.update = obj.getDetails().getUpdateEvent().getTime();
}
}
private List<Entry> mergeEntries(Map<String, List<IObject>> toMerge,
Parameters p, boolean ascending) {
final int swap = ascending ? 1 : -1;
List<Entry> list = new ArrayList<Entry>();
for (String key : toMerge.keySet()) {
for (IObject obj : toMerge.get(key)) {
list.add(new Entry(key, obj));
}
}
Collections.sort(list, new Comparator<Entry>() {
public int compare(Entry o1, Entry o2) {
long u1 = o1.update.getTime();
long u2 = o2.update.getTime();
if (u1 < u2) {
return 1 * swap;
} else if (u2 < u1) {
return -1 * swap;
} else {
return 0;
}
}
});
return list;
}
/**
* Accepts only a properly initialzed {@link Parameters} instance. See
* {@link #applyDefaults(Parameters)}
*/
private List<IObject> mergeList(Map<String, List<IObject>> toMerge,
Parameters p, boolean ascending) {
List<Entry> list = mergeEntries(toMerge, p, ascending);
List<IObject> rv = new ArrayList<IObject>();
int limit = p.theFilter.limit.getValue();
for (int i = 0; i < Math.min(limit, list.size()); i++) {
Entry entry = list.get(i);
rv.add(entry.obj);
}
return rv;
}
/**
* Accepts only a properly initialzed {@link Parameters} instance. See
* {@link #applyDefaults(Parameters)}
*/
private Map<String, List<IObject>> mergeMap(
Map<String, List<IObject>> toMerge, Parameters p,
boolean ascending) {
// Prepare return value so there are no null arrays.
Map<String, List<IObject>> rv = new HashMap<String, List<IObject>>();
for (String key : toMerge.keySet()) {
rv.put(key, new ArrayList<IObject>());
}
List<Entry> list = mergeEntries(toMerge, p, ascending);
toMerge = null;
int limit = p.theFilter.limit.getValue();
for (int i = 0; i < Math.min(limit, list.size()); i++) {
Entry entry = list.get(i);
List<IObject> objs = rv.get(entry.key);
objs.add(entry.obj);
}
return rv;
}
private Parameters applyDefaults(Parameters p) {
if (p == null) {
p = new Parameters();
}
if (p.theFilter == null) {
p.theFilter = new Filter();
}
if (p.theFilter.offset == null) {
p.theFilter.offset = rint(0);
}
if (p.theFilter.limit == null) {
p.theFilter.limit = rint(50);
}
if (p.theFilter.groupId == null) {
if (p.theFilter.ownerId == null) {
p.theFilter.ownerId = rlong(defaultId());
} else if (p.theFilter.ownerId.getValue() == -1L) {
p.theFilter.ownerId = null; // Clearing as wildcard.
}
}
return p;
}
private void applyOwnerGroup(final Parameters p, QueryBuilder qb,
String ownerPath, String groupPath) {
if (p != null && p.theFilter != null) {
Filter f = p.theFilter;
if (f.ownerId != null) {
qb.and(ownerPath + " = :owner_id ");
qb.param("owner_id", f.ownerId.getValue());
}
if (f.groupId != null) {
qb.and(groupPath + " = :group_id ");
qb.param("group_id", f.groupId.getValue());
}
}
}
}