ome.services.ThumbnailCtx.java Source code

Java tutorial

Introduction

Here is the source code for ome.services.ThumbnailCtx.java

Source

/*
 *   $Id$
 *
 *   Copyright 2010 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.perf4j.StopWatch;
import org.perf4j.commonslog.CommonsLogStopWatch;

import ome.api.IPixels;
import ome.api.IQuery;
import ome.api.IRenderingSettings;
import ome.api.IUpdate;
import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
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.model.meta.Session;
import ome.parameters.Parameters;
import ome.security.SecuritySystem;
import ome.system.EventContext;

/**
 *
 */
public class ThumbnailCtx {
    /** Logger for this class. */
    private static final Log log = LogFactory.getLog(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) {
        // 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);
        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 CommonsLogStopWatch("omero.loadAllMetadata");
        List<Thumbnail> toReturn = queryService.findAllByQuery(
                "select t from Thumbnail as t " + "join t.pixels " + "join fetch t.details.updateEvent "
                        + "where t.details.owner.id = :o_id " + "and t.pixels.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 CommonsLogStopWatch("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) {
        Thumbnail thumbnail = pixelsIdMetadataMap.get(pixelsId);
        if (thumbnail == null && securitySystem.isGraphCritical()) {
            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 InternalException(
                    "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);
        }
        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);
        try {
            boolean dirtyMetadata = dirtyMetadata(pixelsId);
            boolean thumbnailExists = thumbnailService.getThumbnailExists(metadata);
            boolean isExtendedGraphCritical = isExtendedGraphCritical(Collections.singleton(pixelsId));
            Long metadataOwnerId = metadata.getDetails().getOwner().getId();
            Long sessionUserId = getCurrentUserId();
            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) {
                log.warn(String.format(
                        "Thumbnail metadata is dirty for Pixels Id:%d and "
                                + "the metadata is owned User id:%d which is not us "
                                + "User id:%d. Ignoring this and returning the cached " + "thumbnail.",
                        pixelsId, metadataOwnerId, sessionUserId));
                return true;
            } else if (thumbnailExists && isExtendedGraphCritical) {
                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.", pixelsId, userId));
                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);
    }

    /**
     * Returns the Id of the currently logged in user.
     * Returns owner of the share while in share
     * @return See above.
     */
    private Long getCurrentUserId() {
        EventContext ec = securitySystem.getEventContext();
        Long shareId = ec.getCurrentShareId();
        if (shareId != null) {
            Session s = queryService.get(Session.class, shareId);
            return s.getOwner().getId();
        }
        return ec.getCurrentUserId();
    }

    /**
     * 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() || 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) {
                    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 CommonsLogStopWatch("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)",
                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 CommonsLogStopWatch("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)",
                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 CommonsLogStopWatch("omero.bulkLoadOwnerRenderingSettings");
        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)",
                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 CommonsLogStopWatch("omero.bulkLoadMetadata");
        List<Thumbnail> toReturn = queryService.findAllByQuery("select t from Thumbnail as t " + "join t.pixels "
                + "join fetch t.details.updateEvent " + "where t.sizeX = :x and t.sizeY = :y "
                + "and t.details.owner.id = :o_id " + "and t.pixels.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 CommonsLogStopWatch("omero.bulkLoadOwnerMetadata");
        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 CommonsLogStopWatch("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 CommonsLogStopWatch("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 CommonsLogStopWatch("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 getThumbnailMetadata()
     */
    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;
    }
}