/*
* Copyright 2010-2014 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services;
import java.awt.Dimension;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import ome.api.IPixels;
import ome.api.IQuery;
import ome.api.IRenderingSettings;
import ome.api.IUpdate;
import ome.conditions.ApiUsageException;
import ome.conditions.ResourceError;
import ome.conditions.ValidationException;
import ome.io.nio.ThumbnailService;
import ome.model.IObject;
import ome.model.core.Image;
import ome.model.core.Pixels;
import ome.model.display.RenderingDef;
import ome.model.display.Thumbnail;
import ome.model.internal.Details;
import ome.model.internal.Permissions;
import ome.parameters.Parameters;
import ome.security.SecuritySystem;
import ome.system.EventContext;
/**
*
*/
public class ThumbnailCtx
{
/**
* Marker exception which is thrown by all methods which wish to tell
* their callers that for whatever reason the desired Thumbnail is not
* available, in which case the caller can use a *Direct method instead.
*
* See <a href="http://trac.openmicroscopy.org/ome/ticket/10618">ticket:10618</a>
*/
public static class NoThumbnail extends Exception {
public NoThumbnail(String message) {
super(message);
}
}
/** Logger for this class. */
private static final Logger log = LoggerFactory.getLogger(ThumbnailCtx.class);
/** Default thumbnail MIME type. */
public static final String DEFAULT_MIME_TYPE = "image/jpeg";
/** OMERO query service. */
private IQuery queryService;
/** OMERO update service. */
private IUpdate updateService;
/** OMERO pixels service. */
private IPixels pixelsService;
/** ROMIO thumbnail service. */
private ThumbnailService thumbnailService;
/** OMERO rendering settings service. */
private IRenderingSettings settingsService;
/** OMERO security system for this session. */
private SecuritySystem securitySystem;
/** User ID to use in queries. */
private long userId;
/** Pixels ID vs. Pixels object map. */
private Map<Long, Pixels> pixelsIdPixelsMap =
new HashMap<Long, Pixels>();
/**
* Pixels ID vs. owner ID map. We don't access these RenderingDef object
* properties directly due to load/unload issues with Hibernate
* (ObjectUnloadedExceptions) when multiple objects were created with or
* updated by the same event.
*/
private Map<Long, Long> pixelsIdOwnerIdMap =
new HashMap<Long, Long>();
/** Pixels ID vs. RenderingDef object map. */
private Map<Long, RenderingDef> pixelsIdSettingsMap =
new HashMap<Long, RenderingDef>();
/** Pixels ID vs. Thumbnail object map. */
private Map<Long, Thumbnail> pixelsIdMetadataMap =
new HashMap<Long, Thumbnail>();
/**
* Pixels ID vs. RenderingDef object last modified time map. We don't access
* these RenderingDef object properties directly due to load/unload issues
* with Hibernate (ObjectUnloadedExceptions) when multiple objects were
* created with or updated by the same event.
*/
private Map<Long, Timestamp> pixelsIdSettingsLastModifiedTimeMap =
new HashMap<Long, Timestamp>();
/**
* Pixels ID vs. Thumbnail object last modified time map. We don't access
* these Thumbnail object properties directly due to load/unload issues
* with Hibernate (ObjectUnloadedExceptions) when multiple objects were
* created with or updated by the same event.
*/
private Map<Long, Timestamp> pixelsIdMetadataLastModifiedTimeMap =
new HashMap<Long, Timestamp>();
/**
* Pixels ID vs. RenderingDef object owner. We don't access these
* RenderingDef object properties directly due to load/unload issues
* with Hibernate (ObjectUnloadedExceptions) when multiple objects were
* created with or updated by the same event.
*/
private Map<Long, Long> pixelsIdSettingsOwnerIdMap =
new HashMap<Long, Long>();
/**
* Default constructor.
* @param queryService OMERO query service to use.
* @param updateService OMERO update service to use.
* @param pixelsService OMERO pixels service to use.
* @param settingsService OMERO rendering settings service to use.
* @param thumbnailService OMERO thumbnail service to use.
* @param securitySystem OMERO security system for this session.
* @param userId Current user ID.
*/
public ThumbnailCtx(IQuery queryService, IUpdate updateService,
IPixels pixelsService, IRenderingSettings settingsService,
ThumbnailService thumbnailService, SecuritySystem securitySystem,
long userId)
{
this.queryService = queryService;
this.updateService = updateService;
this.pixelsService = pixelsService;
this.settingsService = settingsService;
this.thumbnailService = thumbnailService;
this.securitySystem = securitySystem;
this.userId = userId;
}
/**
* Retrieves the current user ID to use for queries.
* @return See above.
*/
public long getUserId()
{
return userId;
}
/**
* Sets the user ID to use for queries.
* @param userId The user ID to use for queries.
*/
public void setUserId(long userId)
{
this.userId = userId;
}
/**
* Bulk loads a set of rendering settings for a group of pixels sets and
* prepares our internal data structures.
* @param pixelsIds Set of Pixels IDs to prepare rendering settings for.
*/
public void loadAndPrepareRenderingSettings(Set<Long> pixelsIds)
{
loadAndPrepareRenderingSettings(Pixels.class, pixelsIds);
}
/**
* Bulk loads a set of rendering settings for a group of pixels sets and
* prepares our internal data structures.
* @param ids Set of IDs to prepare rendering settings for.
* @param klass Either <code>Image</code> or <code>Pixels</code> qualifying
* the type that <code>ids</code> are identifiers for.
*/
private void loadAndPrepareRenderingSettings(Class<? extends IObject> klass,
Set<Long> ids)
{
// Sanity check our ID set
if (ids == null || ids.size() == 0)
{
log.warn("Preparation of null or zero length ID set requested.");
return;
}
// First we need to load our rendering settings either by Image ID or
// by Pixels ID.
List<RenderingDef> settingsList = null;
Set<Long> pixelsIds = null;
if (klass.equals(Pixels.class))
{
// Populate our hash maps asking for our settings by Pixels ID
settingsList = bulkLoadRenderingSettingsByPixelsId(ids);
pixelsIds = ids;
}
else if (klass.equals((Image.class)))
{
// Populate our hash maps asking for our settings by Image ID
settingsList = bulkLoadRenderingSettingsByImageId(ids);
pixelsIds = new HashSet<Long>();
for (RenderingDef def : settingsList)
{
pixelsIds.add(def.getPixels().getId());
}
}
else
{
throw new ApiUsageException(
"Unexpected preparation source type: " + klass.getName());
}
// Now prepare the loaded rendering settings
for (RenderingDef settings : settingsList)
{
prepareRenderingSettings(settings, settings.getPixels());
}
// Locate the Pixels sets we have no settings for this user for.
Set<Long> pixelsIdsWithoutSettings =
getPixelsIdsWithoutSettings(pixelsIds);
// For dimension pooling and checks of graph criticality to work
// correctly for the purpose of thumbnail metadata creation we now need
// to load the Pixels sets that had no rendering settings.
loadMissingPixels(pixelsIdsWithoutSettings);
// Now check to see if we're in a state where missing settings requires
// us to use the owner's settings (we're "graph critical") and load
// them if possible.
if (pixelsIdsWithoutSettings.size() > 0
&& isExtendedGraphCritical(pixelsIdsWithoutSettings))
{
settingsList =
bulkLoadOwnerRenderingSettings(pixelsIdsWithoutSettings);
for (RenderingDef settings : settingsList)
{
prepareRenderingSettings(settings, settings.getPixels());
}
pixelsIdsWithoutSettings = getPixelsIdsWithoutSettings(pixelsIds);
}
}
/**
* Loads and prepares a rendering settings for a Pixels ID and RenderingDef
* ID.
* @param pixelsId Pixels ID to load.
* @param settingsId RenderingDef ID to load an prepare settings for.
*/
public void loadAndPrepareRenderingSettings(long pixelsId, long settingsId)
{
Pixels pixels = pixelsService.retrievePixDescription(pixelsId);
RenderingDef settings = pixelsService.loadRndSettings(settingsId);
if (settings == null)
{
throw new ValidationException(
"No rendering definition exists with ID = " + settingsId);
}
if (!settingsService.sanityCheckPixels(pixels, settings.getPixels()))
{
throw new ValidationException(
"The rendering definition " + settingsId +
" is incompatible with pixels set " + pixels.getId());
}
prepareRenderingSettings(settings, pixels);
}
/**
* Bulk loads and prepares metadata for a group of pixels sets. Calling
* this method guarantees that metadata are available, creating them if
* they are not.
* @param pixelsIds Pixels IDs to prepare metadata for.
* @param longestSide The longest side of the thumbnails requested.
*/
public void loadAndPrepareMetadata(Set<Long> pixelsIds, int longestSide)
{
// Now we're going to attempt to efficiently retrieve the thumbnail
// metadata based on our dimension pools above. To save significant
// time later we're also going to pre-create thumbnail metadata where
// it is missing.
Map<Dimension, Set<Long>> dimensionPools =
createDimensionPools(longestSide);
loadMetadataByDimensionPool(dimensionPools);
createMissingThumbnailMetadata(dimensionPools);
}
/**
* Bulk loads and prepares metadata for a group of pixels sets. Calling
* this method guarantees that metadata are available, creating them if
* they are not.
* @param pixelsIds Pixels IDs to prepare metadata for.
* @param dimensions X-Y dimensions of the thumbnails requested.
*/
public void loadAndPrepareMetadata(Set<Long> pixelsIds,
Dimension dimensions)
{
loadAndPrepareMetadata(pixelsIds, dimensions, true);
}
/**
* Bulk loads and prepares metadata for a group of pixels sets. Calling
* this method guarantees that metadata are available, creating them if
* they are not.
* @param pixelsIds Pixels IDs to prepare metadata for.
* @param dimensions X-Y dimensions of the thumbnails requested.
*/
public void loadAndPrepareMetadata(Set<Long> pixelsIds,
Dimension dimensions,
boolean createMissing)
{
// Now we're going to attempt to efficiently retrieve the thumbnail
// metadata based on our dimension pools above. To save significant
// time later we're also going to pre-create thumbnail metadata where
// it is missing.
Map<Dimension, Set<Long>> dimensionPools =
new HashMap<Dimension, Set<Long>>();
dimensionPools.put(dimensions, pixelsIds);
loadMetadataByDimensionPool(dimensionPools);
if (createMissing)
{
createMissingThumbnailMetadata(dimensionPools);
}
}
/**
* Retrieves all thumbnail metadata available in the database for a given
* Pixels ID.
* @param pixelsId Pixels ID to retrieve thumbnail metadata for.
* @return See above.
*/
public List<Thumbnail> loadAllMetadata(long pixelsId)
{
Parameters params = new Parameters();
params.addId(pixelsId);
params.addLong("o_id", userId);
StopWatch s1 = new Slf4JStopWatch("omero.loadAllMetadata");
List<Thumbnail> toReturn = queryService.findAllByQuery(
"select t from Thumbnail as t " +
"join t.pixels p " +
"join fetch t.details.updateEvent " +
"where t.details.owner.id = :o_id " +
"and p.id = :id", params);
if (toReturn.isEmpty()) {
toReturn = queryService.findAllByQuery(
"select t from Thumbnail as t " +
"join t.pixels p " +
"join fetch t.details.updateEvent " +
"where t.details.owner.id = p.details.owner.id " +
"and p.id = :id", params);
}
s1.stop();
return toReturn;
}
/**
* Resets a given set of Pixels rendering settings to the default
* effectively creating any which do not exist.
* @param pixelsIds Pixels IDs
*/
public void createAndPrepareMissingRenderingSettings(Set<Long> pixelsIds)
{
// Now check to see if we're in a state where missing rendering
// settings and our state requires us to not save.
if (isExtendedGraphCritical(pixelsIds))
{
// TODO: Could possibly "su" to the user and create a thumbnail
return;
}
StopWatch s1 = new Slf4JStopWatch(
"omero.createAndPrepareMissingRenderingSettings");
Set<Long> pixelsIdsWithoutSettings =
getPixelsIdsWithoutSettings(pixelsIds);
int count = pixelsIdsWithoutSettings.size();
if (count > 0)
{
log.info(count + " pixels without settings");
Set<Long> imageIds = settingsService.resetDefaultsInSet(
Pixels.class, pixelsIdsWithoutSettings);
if (count != imageIds.size())
{
log.warn(String.format(
"Return value ID count %d does not match pixels " +
"without settings count %d", imageIds.size(), count));
}
loadAndPrepareRenderingSettings(Image.class, imageIds);
}
s1.stop();
}
/**
* Whether or not settings are available for a given Pixels ID.
* @param pixelsId Pixels ID to check for availability.
* @return <code>true</code> if settings are available and
* <code>false</code> otherwise.
*/
public boolean hasSettings(long pixelsId)
{
return pixelsIdSettingsMap.containsKey(pixelsId);
}
/**
* Whether or not thumbnail metadata is available for a given Pixels ID.
* @param pixelsId Pixels ID to check for availability.
* @return <code>true</code> if metadata is available and
* <code>false</code> otherwise.
*/
public boolean hasMetadata(long pixelsId)
{
return pixelsIdMetadataMap.containsKey(pixelsId);
}
/**
* Retrieves the Pixels object for a given Pixels ID.
* @param pixelsId Pixels ID to retrieve the Pixels object for.
* @return See above.
*/
public Pixels getPixels(long pixelsId)
{
Pixels pixels = pixelsIdPixelsMap.get(pixelsId);
if (pixels == null)
{
throw new ResourceError(String.format(
"Error retrieving Pixels id:%d. Pixels set does not " +
"exist or the user id:%d has insufficient permissions " +
"to retrieve it.", pixelsId, userId));
}
return pixelsIdPixelsMap.get(pixelsId);
}
/**
* Retrieves the RenderingDef object for a given Pixels ID.
* @param pixelsId Pixels ID to retrieve the RenderingDef object for.
* @return See above.
*/
public RenderingDef getSettings(long pixelsId)
{
return pixelsIdSettingsMap.get(pixelsId);
}
/**
* Retrieves the Thumbnail object for a given Pixels ID.
* @param pixelsId Pixels ID to retrieve the Thumbnail object for.
* @return See above.
*/
public Thumbnail getMetadata(long pixelsId) throws NoThumbnail
{
Thumbnail thumbnail = pixelsIdMetadataMap.get(pixelsId);
if (thumbnail == null && securitySystem.isGraphCritical(null)) // maythrow
{
Pixels pixels = pixelsIdPixelsMap.get(pixelsId);
long ownerId = pixels.getDetails().getOwner().getId();
throw new ResourceError(String.format(
"The user id:%s may not be the owner id:%d. The owner " +
"has not viewed the Pixels set id:%d and thumbnail " +
"metadata is missing.", userId, ownerId, pixelsId));
}
else if (thumbnail == null)
{
throw new NoThumbnail(
"Fatal error retrieving thumbnail metadata for Pixels " +
"set id:" + pixelsId);
}
return thumbnail;
}
/**
* Whether or not the thumbnail metadata for a given Pixels ID is dirty
* (the RenderingDef has been updated since the Thumbnail was).
* @param pixelsId Pixels ID to check for dirty metadata.
* @return <code>true</code> if the metadata is dirty <code>false</code>
* otherwise.
*/
public boolean dirtyMetadata(long pixelsId)
{
Timestamp metadataLastUpdated =
pixelsIdMetadataLastModifiedTimeMap.get(pixelsId);
Timestamp settingsLastUpdated =
pixelsIdSettingsLastModifiedTimeMap.get(pixelsId);
if (log.isDebugEnabled())
{
log.debug("Thumb time: " + metadataLastUpdated);
log.debug("Settings time: " + settingsLastUpdated);
}
if (metadataLastUpdated == null) {
return true;
}
return settingsLastUpdated.after(metadataLastUpdated);
}
/**
* Checks to see if a thumbnail is in the on disk cache or not.
*
* @param pixelsId The Pixels set the thumbnail is for.
* @return Whether or not the thumbnail is in the on disk cache.
*/
public boolean isThumbnailCached(long pixelsId)
{
Thumbnail metadata = pixelsIdMetadataMap.get(pixelsId);
if (metadata == null) {
return false;
}
try
{
boolean dirtyMetadata = dirtyMetadata(pixelsId);
boolean thumbnailExists =
thumbnailService.getThumbnailExists(metadata);
boolean isExtendedGraphCritical =
isExtendedGraphCritical(Collections.singleton(pixelsId));
Long metadataOwnerId = metadata.getDetails().getOwner().getId();
Long sessionUserId = securitySystem.getEffectiveUID();
boolean isMyMetadata = sessionUserId.equals(metadataOwnerId);
if (!dirtyMetadata)
{
if (thumbnailExists)
{
return true;
}
else if (!thumbnailExists && isExtendedGraphCritical)
{
throw new ResourceError(String.format(
"Error retrieving Pixels id:%d. Thumbnail " +
"metadata exists but a thumbnail is not " +
"available in the cache. User id:%d has " +
"insufficient permissions to create it.",
pixelsId, userId));
}
}
else if (thumbnailExists && !isMyMetadata)
{
//we need thumbnail for new settings. User creating his own
if (sessionUserId == userId && userId != metadataOwnerId) {
return false;
}
//session user updating someone else thumbnail if allowed
if (userId == metadataOwnerId && sessionUserId != userId) {
return false;
}
log.warn(String.format(
"Thumbnail metadata is dirty for Pixels Id:%d and " +
"the metadata is owned User id:%d which is not " +
"User id:%d. Ignoring this and returning the cached " +
"thumbnail.", pixelsId, metadataOwnerId, userId));
return true;
}
else if (thumbnailExists && isExtendedGraphCritical)
{
if (dirtyMetadata && userId == metadataOwnerId) {
return false;
}
log.warn(String.format(
"Thumbnail metadata is dirty for Pixels Id:%d and " +
"graph is critical for User id:%d. Ignoring this " +
"and returning the cached thumbnail.owner %d dirty %d",
pixelsId, userId, metadataOwnerId, dirtyMetadata));
return true;
}
}
catch (IOException e)
{
String s = "Could not check if thumbnail is cached: ";
log.error(s, e);
throw new ResourceError(s + e.getMessage());
}
return false;
}
/**
* Calculates the ratio of the two sides of a Pixel set and returns the
* X and Y widths based on the longest side maintaining aspect ratio.
*
* @param pixels The Pixels set to calculate against.
* @param longestSide The size of the longest side of the thumbnail
* requested.
* @return The calculated width (X) and height (Y).
*/
public Dimension calculateXYWidths(Pixels pixels, int longestSide)
{
int sizeX = pixels.getSizeX();
int sizeY = pixels.getSizeY();
if (sizeX > sizeY)
{
float ratio = (float) longestSide / sizeX;
return new Dimension(longestSide, (int) (sizeY * ratio));
}
float ratio = (float) longestSide / sizeY;
return new Dimension((int) (sizeX * ratio), longestSide);
}
/**
* Whether or not we're extended graph critical for a given set of
* dimension pools. We're extended graph critical if:
* <ul>
* <li>
* <code>isGraphGritical() == true</code> and the Pixels set does not
* belong to us.
* </li>
* <li>
* <code>isGraphCritical() == false</code>, the Pixels set does not
* belong to us and the group is READ-ONLY.
* </li>
* </ul>
* @param dimensionPools Dimension pools to check if we're graph critical
* for.
* @return <code>true</code> if we're graph critical, and
* <code>false</code> otherwise.
* @see #isExtendedGraphCritical(Set)
*/
private boolean isExtendedGraphCritical(
Map<Dimension, Set<Long>> dimensionPools)
{
for (Set<Long> pool : dimensionPools.values())
{
if (isExtendedGraphCritical(pool))
{
return true;
}
}
return false;
}
/**
* Whether or not we're extended graph critical for a given set of
* dimension pools. We're extended graph critical if:
* <ul>
* <li>
* <code>isGraphGritical() == true</code> and the Pixels set does not
* belong to us.
* </li>
* <li>
* <code>isGraphCritical() == false</code>, the Pixels set does not
* belong to us and the group is READ-ONLY.
* </li>
* </ul>
* @param pixelsIds Set of Pixels to check if we're graph critical for.
* @return <code>true</code> if we're graph critical, and
* <code>false</code> otherwise.
*/
public boolean isExtendedGraphCritical(Set<Long> pixelsIds)
{
EventContext ec = securitySystem.getEventContext();
Permissions currentGroupPermissions = ec.getCurrentGroupPermissions();
Permissions readOnly = Permissions.parseString("rwr---");
if (ec.getCurrentShareId() != null)
{
return true;
}
if (securitySystem.isGraphCritical(null) // May throw
|| currentGroupPermissions.identical(readOnly))
{
for (Long pixelsId : pixelsIds)
{
// Check if the Pixels ID vs. Owner ID map is missing, which
// signifies that we were completely unable to load any of the
// Pixels sets identified in pixelsIds.
if (pixelsIdOwnerIdMap == null) {
throw new ResourceError(String.format(
"Error retrieving Pixels id:%d. Pixels set does " +
"not exist or the user id:%d has insufficient " +
"permissions to retrieve it.", pixelsId, userId));
}
Long pixelsOwner = pixelsIdOwnerIdMap.get(pixelsId);
// Check if the Owner ID is missing from the map, which as
// above signifies that we unable to load this particular
// Pixels set as identified by Pixels ID. This will be a hard
// failure due to the crazy state that this potentially
// suggests.
if (pixelsOwner == null) {
throw new ResourceError(String.format(
"Error retrieving Pixels id:%d. Pixels set does " +
"not exist or the user id:%d has insufficient " +
"permissions to retrieve it.", pixelsId, userId));
}
if (pixelsOwner != userId &&
!(ec.getLeaderOfGroupsList().contains(userId) ||
ec.isCurrentUserAdmin()))
{
return true;
}
}
}
return false;
}
/**
* Creates X-Y dimension pools based on a requested longest side.
* @param longestSide Requested longest side of the thumbnail.
* @return Map of X-Y dimension vs. Pixels ID (a set of dimension pools).
*/
private Map<Dimension, Set<Long>> createDimensionPools(int longestSide)
{
Map<Dimension, Set<Long>> dimensionPools =
new HashMap<Dimension, Set<Long>>();
for (Pixels pixels : pixelsIdPixelsMap.values())
{
// Calculate the XY widths we would use for a thumbnail of Pixels
Dimension dimensions = calculateXYWidths(pixels, longestSide);
addToDimensionPool(dimensionPools, pixels, dimensions);
}
return dimensionPools;
}
/**
* Adds the Id of a particular set of Pixels to the correct dimension pool
* based on the requested longest side.
*
* @param pools Map of the current dimension pools.
* @param pixels Pixels set to add to the correct dimension pool.
* @param dimensions Dimensions pool to add to.
*/
private void addToDimensionPool(Map<Dimension, Set<Long>> pools,
Pixels pixels, Dimension dimensions)
{
// Insert the Pixels set into the dimension pool
Set<Long> pool = pools.get(dimensions);
if (pool == null)
{
pool = new HashSet<Long>();
}
pool.add(pixels.getId());
pools.put(dimensions, pool);
}
/**
* Examines the currently prepared data structures for Pixels IDs without
* settings.
* @param pixelsIds Pixels IDs to check.
* @return Set of Pixels IDs which do not have settings prepared.
*/
private Set<Long> getPixelsIdsWithoutSettings(Set<Long> pixelsIds)
{
Set<Long> pixelsIdsWithoutSettings = new HashSet<Long>();
for (Long pixelsId : pixelsIds)
{
if (!hasSettings(pixelsId))
{
pixelsIdsWithoutSettings.add(pixelsId);
}
}
return pixelsIdsWithoutSettings;
}
/**
* Examines the currently prepared data structures for Pixels IDs without
* thumbnail metadata.
* @param pixelsIds Pixels IDs to check.
* @return Set of Pixels IDs which do not have thumbnail metadata prepared.
*/
private Set<Long> getPixelsIdsWithoutMetadata(Set<Long> pixelsIds)
{
Set<Long> pixelsIdsWithoutMetadata = new HashSet<Long>();
for (Long pixelsId : pixelsIds)
{
if (!hasMetadata(pixelsId))
{
pixelsIdsWithoutMetadata.add(pixelsId);
}
}
return pixelsIdsWithoutMetadata;
}
/**
* Bulk loads a set of rendering sets for a group of pixels sets.
* @param pixelsIds the Pixels sets to retrieve thumbnails for.
* @return Loaded rendering settings for <code>pixelsIds</code>.
*/
private List<RenderingDef> bulkLoadRenderingSettingsByPixelsId(
Set<Long> pixelsIds)
{
StopWatch s1 = new Slf4JStopWatch(
"omero.bulkLoadRenderingSettings");
List<RenderingDef> toReturn = queryService.findAllByQuery(
"select r from RenderingDef as r " +
"join fetch r.pixels as p " +
"join fetch r.details.updateEvent " +
"join p.details.updateEvent " +
"where r.details.owner.id = :id and r.pixels.id in (:ids) " +
"order by r.details.updateEvent.time asc",
new Parameters().addId(userId).addIds(pixelsIds));
if (toReturn.isEmpty()) {
toReturn = queryService.findAllByQuery(
"select r from RenderingDef as r " +
"join fetch r.pixels as p " +
"join fetch r.details.updateEvent " +
"join p.details.updateEvent " +
"where r.details.owner.id = p.details.owner.id " +
"and r.pixels.id in (:ids) " +
"order by r.details.updateEvent.time asc",
new Parameters().addId(userId).addIds(pixelsIds));
}
s1.stop();
return toReturn;
}
/**
* Bulk loads a set of rendering sets for a group of Images.
* @param imageIds the Images retrieve thumbnails for.
* @return Loaded rendering settings for <code>imageIds</code>.
*/
private List<RenderingDef> bulkLoadRenderingSettingsByImageId(
Set<Long> imageIds)
{
StopWatch s1 = new Slf4JStopWatch(
"omero.bulkLoadRenderingSettings");
List<RenderingDef> toReturn = queryService.findAllByQuery(
"select r from RenderingDef as r " +
"join fetch r.pixels as p " +
"join fetch r.details.updateEvent " +
"join fetch p.details.updateEvent " +
"where r.details.owner.id = :id " +
"and r.pixels.image.id in (:ids) " +
"order by r.details.updateEvent.time asc",
new Parameters().addId(userId).addIds(imageIds));
if (toReturn.isEmpty()) {
toReturn = queryService.findAllByQuery(
"select r from RenderingDef as r " +
"join fetch r.pixels as p " +
"join fetch r.details.updateEvent " +
"join fetch p.details.updateEvent " +
"where r.details.owner.id = p.details.owner.id " +
"and r.pixels.image.id in (:ids) " +
"order by r.details.updateEvent.time asc",
new Parameters().addId(userId).addIds(imageIds));
}
s1.stop();
return toReturn;
}
/**
* Bulk loads a set of rendering sets for a group of pixels sets.
* @param pixelsIds the Pixels sets to retrieve thumbnails for.
* @return Loaded rendering settings for <code>pixelsIds</code>.
*/
private List<RenderingDef> bulkLoadOwnerRenderingSettings(
Set<Long> pixelsIds)
{
StopWatch s1 = new Slf4JStopWatch(
"omero.bulkLoadOwnerRenderingSettings");
// Why doesn't this first try by userId?
List<RenderingDef> toReturn = queryService.findAllByQuery(
"select r from RenderingDef as r " +
"join fetch r.pixels as p " +
"join fetch r.details.updateEvent " +
"join fetch p.details.updateEvent " +
"where r.details.owner.id = p.details.owner.id " +
"and r.pixels.id in (:ids) " +
"order by r.details.updateEvent.time asc",
new Parameters().addIds(pixelsIds));
s1.stop();
return toReturn;
}
/**
* Bulk loads thumbnail metadata.
* @param dimensions X-Y dimensions to bulk load metdata for.
* @param pixelsIds Pixels IDs to bulk load metadata for.
* @return List of thumbnail objects with <code>thumbnail.pixels</code> and
* <code>thumbnail.details.updateEvent</code> loaded.
*/
private List<Thumbnail> bulkLoadMetadata(Dimension dimensions,
Set<Long> pixelsIds)
{
Parameters params = new Parameters();
params.addInteger("x", (int) dimensions.getWidth());
params.addInteger("y", (int) dimensions.getHeight());
params.addLong("o_id", userId);
params.addIds(pixelsIds);
StopWatch s1 = new Slf4JStopWatch("omero.bulkLoadMetadata");
List<Thumbnail> toReturn = queryService.findAllByQuery(
"select t from Thumbnail as t " +
"join t.pixels p " +
"join fetch t.details.updateEvent " +
"where t.sizeX = :x and t.sizeY = :y " +
"and t.details.owner.id = :o_id " +
"and p.id in (:ids)", params);
if (toReturn.isEmpty()) {
toReturn = queryService.findAllByQuery(
"select t from Thumbnail as t " +
"join t.pixels p " +
"join fetch t.details.updateEvent " +
"where t.sizeX = :x and t.sizeY = :y " +
"and t.details.owner.id = p.details.owner.id " +
"and p.id in (:ids)", params);
}
s1.stop();
return toReturn;
}
/**
* Bulk loads thumbnail metadata that is owned by the owner of the Pixels
* set..
* @param dimensions X-Y dimensions to bulk load metadata for.
* @param pixelsIds Pixels IDs to bulk load metadata for.
* @return List of thumbnail objects with <code>thumbnail.pixels</code> and
* <code>thumbnail.details.updateEvent</code> loaded.
*/
private List<Thumbnail> bulkLoadOwnerMetadata(Dimension dimensions,
Set<Long> pixelsIds)
{
Parameters params = new Parameters();
params.addInteger("x", (int) dimensions.getWidth());
params.addInteger("y", (int) dimensions.getHeight());
params.addIds(pixelsIds);
StopWatch s1 = new Slf4JStopWatch("omero.bulkLoadOwnerMetadata");
// Why is does this not try userId first?
List<Thumbnail> toReturn = queryService.findAllByQuery(
"select t from Thumbnail as t " +
"join t.pixels as p " +
"join fetch t.details.updateEvent " +
"where t.sizeX = :x and t.sizeY = :y " +
"and t.details.owner.id = p.details.owner.id " +
"and t.pixels.id in (:ids)", params);
s1.stop();
return toReturn;
}
/**
* Attempts to efficiently retrieve the thumbnail metadata based on a set
* of dimension pools. At worst, the result of maintaining the aspect ratio
* (calculating the new XY widths) is that we have to retrieve each
* thumbnail object separately.
* @param dimensionPools Dimension pools to query based upon.
* @param metadataMap Dictionary of Pixels ID vs. thumbnail metadata. Will
* be updated by this method.
* @param metadataTimeMap Dictionary of Pixels ID vs. thumbnail metadata
* last modification time. Will be updated by this method.
*/
private void loadMetadataByDimensionPool(
Map<Dimension, Set<Long>> dimensionPools)
{
StopWatch s1 = new Slf4JStopWatch(
"omero.loadMetadataByDimensionPool");
for (Dimension dimensions : dimensionPools.keySet())
{
Set<Long> pool = dimensionPools.get(dimensions);
// First populate our hash maps asking for our metadata.
List<Thumbnail> thumbnailList = bulkLoadMetadata(dimensions, pool);
for (Thumbnail metadata : thumbnailList)
{
prepareMetadata(metadata, metadata.getPixels().getId());
}
// Now check to see if we're in a state where missing metadata
// requires us to use the owner's metadata (we're "graph critical")
// and load them if possible.
Set<Long> pixelsIdsWithoutMetadata =
getPixelsIdsWithoutMetadata(pool);
if (pixelsIdsWithoutMetadata.size() > 0
&& isExtendedGraphCritical(pixelsIdsWithoutMetadata))
{
thumbnailList = bulkLoadOwnerMetadata(
dimensions, pixelsIdsWithoutMetadata);
for (Thumbnail metadata : thumbnailList)
{
prepareMetadata(metadata, metadata.getPixels().getId());
}
}
}
s1.stop();
}
/**
* Loads and prepares missing Pixels sets.
* @param pixelsIds Pixels IDs to load missing Pixels objects for.
*/
private void loadMissingPixels(Set<Long> pixelsIds)
{
if (pixelsIds.size() > 0)
{
Parameters parameters = new Parameters();
parameters.addIds(pixelsIds);
if (log.isDebugEnabled())
{
log.debug("Loading " + pixelsIds.size() + " missing Pixels.");
}
StopWatch s1 = new Slf4JStopWatch(
"omero.loadMissingPixels");
List<Pixels> pixelsWithoutSettings = queryService.findAllByQuery(
"select p from Pixels as p where id in (:ids)", parameters);
s1.stop();
for (Pixels pixels : pixelsWithoutSettings)
{
Long pixelsId = pixels.getId();
pixelsIdPixelsMap.put(pixelsId, pixels);
pixelsIdOwnerIdMap.put(
pixelsId, pixels.getDetails().getOwner().getId());
}
}
}
/**
* Prepares a set of rendering settings, extracting relevant metadata and
* preparing the internal maps.
* @param settings RenderingDef object to prepare.
* @param pixels Pixels object to prepare.
*/
private void prepareRenderingSettings(RenderingDef settings, Pixels pixels)
{
Long pixelsId = pixels.getId();
pixelsIdPixelsMap.put(pixelsId, pixels);
pixelsIdOwnerIdMap.put(pixelsId, pixels.getDetails().getOwner().getId());
Details details = settings.getDetails();
Timestamp timestemp = details.getUpdateEvent().getTime();
pixelsIdSettingsMap.put(pixelsId, settings);
pixelsIdSettingsLastModifiedTimeMap.put(pixelsId, timestemp);
pixelsIdSettingsOwnerIdMap.put(pixelsId, details.getOwner().getId());
}
/**
* Prepares thumbnail metadata extracting relevant metadata and prepares
* the internal maps.
* @param metadata Thumbnail object to prepare.
* @param pixelsId Pixels ID to prepare.
*/
private void prepareMetadata(Thumbnail metadata, long pixelsId)
{
Timestamp t = metadata.getDetails().getUpdateEvent().getTime();
pixelsIdMetadataMap.put(pixelsId, metadata);
pixelsIdMetadataLastModifiedTimeMap.put(pixelsId, t);
}
/**
* Creates missing thumbnail metadata for a set of Pixels IDs that have
* been prepared.
* @param dimensionPools Dimension pools to retrieve pre-calculated,
* requested dimensions from.
*/
private void createMissingThumbnailMetadata(
Map<Dimension, Set<Long>> dimensionPools)
{
// Now check to see if we're in a state where missing metadata
// and our state requires us to not save.
if (isExtendedGraphCritical(dimensionPools))
{
// TODO: Could possibly "su" to the user and create a thumbnail
return;
}
StopWatch s1 = new Slf4JStopWatch(
"omero.createMissingThumbnailMetadata");
List<Thumbnail> toSave = new ArrayList<Thumbnail>();
Map<Dimension, Set<Long>> temporaryDimensionPools =
new HashMap<Dimension, Set<Long>>();
Set<Long> pixelsIdsWithoutMetadata =
getPixelsIdsWithoutMetadata(pixelsIdPixelsMap.keySet());
for (Long pixelsId : pixelsIdsWithoutMetadata)
{
Pixels pixels = pixelsIdPixelsMap.get(pixelsId);
for (Dimension dimension : dimensionPools.keySet())
{
Set<Long> pool = dimensionPools.get(dimension);
if (pool.contains(pixelsId))
{
toSave.add(createThumbnailMetadata(pixels, dimension));
addToDimensionPool(
temporaryDimensionPools, pixels, dimension);
break;
}
}
}
log.info("New thumbnail object set size: " + toSave.size());
log.info("Dimension pool size: " + temporaryDimensionPools.size());
if (toSave.size() > 0)
{
updateService.saveAndReturnIds(
toSave.toArray(new Thumbnail[toSave.size()]));
loadMetadataByDimensionPool(temporaryDimensionPools);
}
s1.stop();
}
/**
* Creates metadata for a thumbnail of a given set of pixels set and X-Y
* dimensions.
*
* @param pixels The Pixels set to create thumbnail metadata for.
* @param dimensions The dimensions of the thumbnail.
*
* @return the thumbnail metadata as created.
* @see #getMetadata(long)
*/
public Thumbnail createThumbnailMetadata(Pixels pixels,
Dimension dimensions)
{
// Unload the pixels object to avoid transactional headaches
Pixels unloadedPixels = new Pixels(pixels.getId(), false);
Thumbnail thumb = new Thumbnail();
thumb.setPixels(unloadedPixels);
thumb.setMimeType(DEFAULT_MIME_TYPE);
thumb.setSizeX((int) dimensions.getWidth());
thumb.setSizeY((int) dimensions.getHeight());
return thumb;
}
}