diff --git a/components/blitz/resources/omero/api/ThumbnailStore.ice b/components/blitz/resources/omero/api/ThumbnailStore.ice index 9933f641836..47ba7d0262e 100644 --- a/components/blitz/resources/omero/api/ThumbnailStore.ice +++ b/components/blitz/resources/omero/api/ThumbnailStore.ice @@ -90,7 +90,8 @@ module omero { * in the on-disk cache it will be returned directly, * otherwise it will be created as in * {@link #getThumbnailDirect}, placed in the on-disk - * cache and returned. + * cache and returned. If the thumbnail is missing, a clock will + * be returned to signify that the thumbnail is yet to be generated. * * @param sizeX the X-axis width of the thumbnail. * null specifies the default size @@ -112,6 +113,35 @@ module omero { */ idempotent Ice::ByteSeq getThumbnail(omero::RInt sizeX, omero::RInt sizeY) throws ServerError; + /** + * Retrieves a thumbnail for a pixels set using a given set of + * rendering settings (RenderingDef). If the thumbnail exists + * in the on-disk cache it will be returned directly, + * otherwise it will be created as in + * {@link #getThumbnailDirect}, placed in the on-disk + * cache and returned. If the thumbnail is still to be generated, an empty array will + * be returned. + * + * @param sizeX the X-axis width of the thumbnail. + * null specifies the default size + * of 48. + * @param sizeY the Y-axis width of the thumbnail. + * null specifies the default size + * of 48. + * @throws ApiUsageException + * if: + * + * @return a JPEG thumbnail byte buffer + * @see #getThumbnailDirect + */ + idempotent Ice::ByteSeq getThumbnailWithoutDefault(omero::RInt sizeX, omero::RInt sizeY) throws ServerError; + /** * Retrieves a number of thumbnails for pixels sets using * given sets of rendering settings (RenderingDef). If the diff --git a/components/blitz/src/ome/services/blitz/impl/ThumbnailStoreI.java b/components/blitz/src/ome/services/blitz/impl/ThumbnailStoreI.java index ee645fb8fb1..be568535cff 100644 --- a/components/blitz/src/ome/services/blitz/impl/ThumbnailStoreI.java +++ b/components/blitz/src/ome/services/blitz/impl/ThumbnailStoreI.java @@ -16,6 +16,7 @@ import omero.api.AMD_ThumbnailStore_createThumbnailsByLongestSideSet; import omero.api.AMD_ThumbnailStore_getRenderingDefId; import omero.api.AMD_ThumbnailStore_getThumbnail; +import omero.api.AMD_ThumbnailStore_getThumbnailWithoutDefault; import omero.api.AMD_ThumbnailStore_getThumbnailByLongestSide; import omero.api.AMD_ThumbnailStore_getThumbnailByLongestSideDirect; import omero.api.AMD_ThumbnailStore_getThumbnailByLongestSideSet; @@ -126,6 +127,12 @@ public void getThumbnail_async(AMD_ThumbnailStore_getThumbnail __cb, } + public void getThumbnailWithoutDefault_async(AMD_ThumbnailStore_getThumbnailWithoutDefault __cb, + RInt sizeX, RInt sizeY, Current __current) throws ServerError { + callInvokerOnRawArgs(__cb, __current, sizeX, sizeY); + + } + public void resetDefaults_async(AMD_ThumbnailStore_resetDefaults __cb, Current __current) throws ServerError { callInvokerOnRawArgs(__cb, __current); diff --git a/components/blitz/test/ome/services/blitz/test/utests/IceMethodInvokerUnitTest.java b/components/blitz/test/ome/services/blitz/test/utests/IceMethodInvokerUnitTest.java index 00444a58612..c4a22162c57 100644 --- a/components/blitz/test/ome/services/blitz/test/utests/IceMethodInvokerUnitTest.java +++ b/components/blitz/test/ome/services/blitz/test/utests/IceMethodInvokerUnitTest.java @@ -233,6 +233,11 @@ public long getRenderingDefId() { return -1; } + @Override + public byte[] getThumbnailWithoutDefault(Integer arg0, Integer arg1) { + return null; + } + } // diff --git a/components/common/src/ome/api/ThumbnailStore.java b/components/common/src/ome/api/ThumbnailStore.java index f2048a36f28..3811c447457 100644 --- a/components/common/src/ome/api/ThumbnailStore.java +++ b/components/common/src/ome/api/ThumbnailStore.java @@ -105,6 +105,35 @@ public interface ThumbnailStore extends StatefulServiceInterface { * @see #getThumbnailDirect(Integer, Integer) */ public byte[] getThumbnail(Integer sizeX, Integer sizeY); + + /** + * Retrieves a thumbnail for a pixels set using a given set of + * rendering settings (RenderingDef). If the thumbnail exists + * in the on-disk cache it will be returned directly, + * otherwise it will be created as in + * {@link #getThumbnailDirect}, placed in the on-disk + * cache and returned. If the thumbnail is still to be generated, an empty array will + * be returned. + * + * @param sizeX the X-axis width of the thumbnail. + * null specifies the default size + * of 48. + * @param sizeY the Y-axis width of the thumbnail. + * null specifies the default size + * of 48. + * @throws ApiUsageException + * if: + * + * @return a JPEG thumbnail byte buffer + * @see #getThumbnailDirect + */ + public byte[] getThumbnailWithoutDefault(Integer sizeX, Integer sizeY); /** * Retrieves a number of thumbnails for pixels sets using given sets of diff --git a/components/insight/SRC/org/openmicroscopy/shoola/env/data/model/ThumbnailData.java b/components/insight/SRC/org/openmicroscopy/shoola/env/data/model/ThumbnailData.java index 73149962abb..e226f9e3004 100644 --- a/components/insight/SRC/org/openmicroscopy/shoola/env/data/model/ThumbnailData.java +++ b/components/insight/SRC/org/openmicroscopy/shoola/env/data/model/ThumbnailData.java @@ -11,7 +11,7 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. @@ -22,12 +22,14 @@ package org.openmicroscopy.shoola.env.data.model; import java.awt.Graphics2D; +import java.awt.Image; import java.awt.image.BufferedImage; import omero.gateway.model.ImageData; +import org.openmicroscopy.shoola.util.image.geom.Factory; -/** +/** * Holds a {@link BufferedImage} serving as a thumbnail for a given OME * image. * @@ -45,79 +47,79 @@ public class ThumbnailData /** The id of the image to which the thumbnail belong. */ private long userID; - + /** The id of the image to which the thumbnail belong. */ private long imageID; - + /** The thumbnail pixels. */ private BufferedImage thumbnail; /** Flag indicating if the image is a default image or not. */ private boolean validImage; - + /** Used to store the image. */ private ImageData image; - + /** Flag indicating that the image required a pyramid to be build.*/ private Boolean requirePyramid; - + /** The object of reference. */ private omero.gateway.model.DataObject refObject; - - /** + + /** * Exception if not possible to create the object. This should * be used when imported. */ private Exception error; - + /** * Creates a new instance. - * + * * @param imageID The id of the image to which the thumbnail belong. * Must be positive. * @param thumbnail The thumbnail pixels. Mustn't be null. * @param userID The id of the user the thumbnail is for. * Must be positive. - * @param validImage Pass true if the image is a real image, + * @param validImage Pass true if the image is a real image, * false otherwise. */ - public ThumbnailData(long imageID, BufferedImage thumbnail, long userID, - boolean validImage) + public ThumbnailData(long imageID, Image thumbnail, long userID, + boolean validImage) { - if (imageID <= 0) + if (imageID <= 0) throw new IllegalArgumentException("Non-positive image id: "+ imageID+"."); this.imageID = imageID; - this.thumbnail = thumbnail; + this.thumbnail = toBufferedImage(thumbnail); this.userID = userID; this.validImage = validImage; requirePyramid = null; } - + /** * Creates a new instance. - * + * * @param imageID The id of the image to which the thumbnail belong. * Must be positive. * @param thumbnail The thumbnail pixels. Mustn't be null. - * @param validImage Pass true if the image is a real image, + * @param validImage Pass true if the image is a real image, * false otherwise. */ - public ThumbnailData(long imageID, BufferedImage thumbnail, + public ThumbnailData(long imageID, Image thumbnail, boolean validImage) { this(imageID, thumbnail, -1, validImage); } - + /** * Creates a new instance. - * + * * @param refOjbect The object of reference. Mustn't be null. * @param thumbnail The thumbnail pixels. Mustn't be null. - * @param validImage Passed true if it is a valid image, + * @param validImage Passed true if it is a valid image, * false otherwise. */ - public ThumbnailData(omero.gateway.model.DataObject refOjbect, BufferedImage thumbnail, + public ThumbnailData(omero.gateway.model.DataObject refOjbect, Image thumbnail, boolean validImage) { if (refOjbect == null) @@ -126,13 +128,13 @@ public ThumbnailData(omero.gateway.model.DataObject refOjbect, BufferedImage thu throw new IllegalArgumentException("Type not valid."); this.refObject = refOjbect; this.validImage = validImage; - this.thumbnail = thumbnail; + this.thumbnail = toBufferedImage(thumbnail); requirePyramid = null; } - + /** * Creates a new instance. - * + * * @param refOjbect The object of reference. Mustn't be null. * @param requirePyramid Pass true if a pyramid is required, * false otherwise. @@ -142,10 +144,10 @@ public ThumbnailData(omero.gateway.model.DataObject refOjbect, Boolean requirePy this(refOjbect, null, false); this.requirePyramid = requirePyramid; } - + /** * Creates a new instance. - * + * * @param refOjbect The object of reference. Mustn't be null. * @param thumbnail The thumbnail pixels. Mustn't be null. */ @@ -153,43 +155,43 @@ public ThumbnailData(omero.gateway.model.DataObject refOjbect, BufferedImage thu { this(refOjbect, thumbnail, true); } - + /** * Sets the time the flag indicating if the image requires a pyramid to be * built. - * + * * @param requirePyramid The value to set. */ public void setBackOffForPyramid(Boolean requirePyramid) { this.requirePyramid = requirePyramid; } - + /** * Sets the exception thrown when trying to create a thumbnail. - * + * * @param error The exception to set. */ public void setError(Exception error) { this.error = error; } - + /** * Returns the exception. - * + * * @return See above. */ public Exception getError() { return error; } - - /** + + /** * Sets the image. - * + * * @param image The image to set. */ public void setImage(ImageData image) { this.image = image; } - + /** * Clones this object. * This is a deep-copy, the thumbnail pixels are cloned too. - * + * * @see org.openmicroscopy.shoola.env.data.model.DataObject#makeNew() */ public DataObject makeNew() @@ -197,7 +199,7 @@ public DataObject makeNew() BufferedImage pixClone = null; if (thumbnail != null) { pixClone = new BufferedImage( thumbnail.getWidth(), - thumbnail.getHeight(), + thumbnail.getHeight(), thumbnail.getType()); Graphics2D g2D = pixClone.createGraphics(); g2D.drawImage(thumbnail, null, 0, 0); @@ -216,58 +218,71 @@ public DataObject makeNew() } /** - * Returns true if the image is a real image, + * Returns true if the image is a real image, * false otherwise. - * + * * @return See above. */ public boolean isValidImage() { return validImage; } - + /** * Returns the id of the user. - * + * * @return See above. */ public long getUserID() { return userID; } - + /** * Returns the id of the image to which the thumbnail belong. - * + * * @return See above. */ public long getImageID() { return imageID; } - + /** * Returns the thumbnail pixels. - * + * * @return See above. */ public BufferedImage getThumbnail() { return thumbnail; } - + /** * Returns the image. - * + * * @return See above. */ public ImageData getImage() { return image; } - + /** * Returns the object of reference. - * + * * @return See above. */ public omero.gateway.model.DataObject getRefObject() { return refObject; } - + /** * Returns true if a pyramid is required, false * otherwise. - * + * * @return See above. */ public Boolean requirePyramid() - { + { if (requirePyramid != null) return requirePyramid.booleanValue(); return null; } + /** + * Converts a given Image into a BufferedImage + * + * @param img The Image to be converted + * @return The converted BufferedImage + */ + public static BufferedImage toBufferedImage(Image img) { + if (img instanceof BufferedImage) { + return (BufferedImage) img; + } + return Factory.createImage(img); + } + } diff --git a/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/DataManagerViewImpl.java b/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/DataManagerViewImpl.java index b0b332f6f45..901d1dfddae 100644 --- a/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/DataManagerViewImpl.java +++ b/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/DataManagerViewImpl.java @@ -141,8 +141,7 @@ public CallHandle countContainerItems(SecurityContext ctx, Set rootIDs, public CallHandle loadThumbnail(SecurityContext ctx, ImageData image, int maxWidth, int maxHeight, long userID, AgentEventListener observer) { - BatchCallTree cmd = new ThumbnailLoader(ctx, image, maxWidth, maxHeight, - userID); + BatchCallTree cmd = new ThumbnailLoader(ctx, image, maxWidth, maxHeight, userID); return cmd.exec(observer); } diff --git a/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/HierarchyBrowsingViewImpl.java b/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/HierarchyBrowsingViewImpl.java index 9b232bf4919..93394ddc637 100644 --- a/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/HierarchyBrowsingViewImpl.java +++ b/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/HierarchyBrowsingViewImpl.java @@ -65,18 +65,18 @@ public CallHandle loadHierarchy(SecurityContext ctx, Class rootNodeType, /** * Implemented as specified by the view interface. - * @see HierarchyBrowsingView#loadThumbnails(Collection, int, int, long, - * AgentEventListener) + * @see HierarchyBrowsingView#loadThumbnails(SecurityContext, Collection, int, int, long, int, AgentEventListener) */ public CallHandle loadThumbnails(SecurityContext ctx, Collection images, int maxWidth, int maxHeight, long userID, int type, AgentEventListener observer) { BatchCallTree cmd; - if (type == EXPERIMENTER) - cmd = new ThumbnailSetLoader(ctx, images, maxHeight, type); - else cmd = new ThumbnailLoader(ctx, images, maxWidth, - maxHeight, userID); + if (type == EXPERIMENTER) { + cmd = new ThumbnailSetLoader(ctx, images, maxHeight, type); + } else { + cmd = new ThumbnailLoader(ctx, images, maxWidth, maxHeight, userID); + } return cmd.exec(observer); } diff --git a/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/calls/ThumbnailLoader.java b/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/calls/ThumbnailLoader.java index 6d4708dba24..c46800a8bc6 100644 --- a/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/calls/ThumbnailLoader.java +++ b/components/insight/SRC/org/openmicroscopy/shoola/env/data/views/calls/ThumbnailLoader.java @@ -11,7 +11,7 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. @@ -21,38 +21,35 @@ package org.openmicroscopy.shoola.env.data.views.calls; -import java.awt.Dimension; -import java.awt.image.BufferedImage; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - +import ome.conditions.ResourceError; import omero.ServerError; - +import omero.api.IConfigPrx; +import omero.api.RawPixelsStorePrx; import omero.api.ThumbnailStorePrx; - +import omero.gateway.SecurityContext; +import omero.gateway.exception.DSAccessException; +import omero.gateway.exception.DSOutOfServiceException; +import omero.gateway.model.DataObject; +import omero.gateway.model.ImageData; +import omero.gateway.model.PixelsData; +import omero.log.LogMessage; import org.openmicroscopy.shoola.env.data.OmeroImageService; import org.openmicroscopy.shoola.env.data.model.ThumbnailData; - -import omero.gateway.SecurityContext; -import omero.gateway.exception.RenderingServiceException; - import org.openmicroscopy.shoola.env.data.views.BatchCall; import org.openmicroscopy.shoola.env.data.views.BatchCallTree; - -import omero.log.LogMessage; - +import org.openmicroscopy.shoola.util.VersionCompare; import org.openmicroscopy.shoola.util.image.geom.Factory; import org.openmicroscopy.shoola.util.image.io.WriterImage; -import omero.gateway.model.DataObject; -import omero.gateway.model.ImageData; -import omero.gateway.model.PixelsData; +import java.awt.Dimension; +import java.awt.Image; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; -/** +/** * Command to load a given set of thumbnails. - *

As thumbnails are retrieved from OMERO, they're posted back to the + *

As thumbnails are retrieved from OMERO, they're posted back to the * caller through DSCallFeedbackEvents. Each thumbnail will be * posted in a single event; the caller can then invoke the * getPartialResult method to retrieve a ThumbnailData @@ -62,395 +59,305 @@ * original image and so that their area doesn't exceed maxWidth* * maxHeight, which is specified to the constructor.

* - * @author Jean-Marie Burel      - * j.burel@dundee.ac.uk - * @author
Andrea Falconi      - * - * a.falconi@dundee.ac.uk + * @author Jean-Marie Burel      + * j.burel@dundee.ac.uk + * @author
Andrea Falconi      + * + * a.falconi@dundee.ac.uk * @version 2.2 * @since OME2.2 */ -public class ThumbnailLoader - extends BatchCallTree -{ +public class ThumbnailLoader extends BatchCallTree { + + /** + * Version of OMERO.server with {@code getThumbnailWithoutDefault} + */ + private static final String VERSION_THUMBNAIL_NO_DEFAULT = "5.4.8"; - /** The images for which we need thumbnails. */ + /** + * The images for which we need thumbnails. + */ private Collection images; - /** The maximum acceptable width of the thumbnails. */ + /** + * The maximum acceptable width of the thumbnails. + */ private int maxWidth; - /** The maximum acceptable height of the thumbnails. */ + /** + * The maximum acceptable height of the thumbnails. + */ private int maxHeight; - /** The lastly retrieved thumbnail. */ + /** + * The lastly retrieved thumbnail. + */ private Object currentThumbnail; - /** Flag to indicate if the class was invoked for a pixels ID. */ - private boolean pixelsCall; - - /** The id of the pixels set this loader is for. */ - private long pixelsID; - - /** Collection of user IDs. */ - private Set userIDs; - - /** Helper reference to the image service. */ - private OmeroImageService service; - - /** Load the thumbnail as an full size image. */ - private boolean asImage; - - /** The security context.*/ - private SecurityContext ctx; - /** - * Loads the thumbnail for {@link #images}[index]. - * - * @param pxd The image the thumbnail for. - * @param userID The id of the user the thumbnail is for. - * @param store The thumbnail store to use. - * @param imageID The id of the image associated to the pixels set. + * Collection of user IDs. */ - private void loadThumbail(PixelsData pxd, long userID, - ThumbnailStorePrx store, boolean last, long imageID) - { - BufferedImage thumbPix = null; - boolean valid = true; - int sizeX = maxWidth, sizeY = maxHeight; - try { - if (asImage) { - sizeX = pxd.getSizeX(); - sizeY = pxd.getSizeY(); - } else { - Dimension d = Factory.computeThumbnailSize(sizeX, sizeY, - pxd.getSizeX(), pxd.getSizeY()); - sizeX = d.width; - sizeY = d.height; - } - - if (!store.setPixelsId(pxd.getId())) { - store.resetDefaults(); - store.setPixelsId(pxd.getId()); - } - if (userID >= 0) { - long rndDefId = service.getRenderingDef(ctx, - pxd.getId(), userID); - // the user might not have own rendering settings - // for this image - if (rndDefId >= 0) - store.setRenderingDefId(rndDefId); - } - thumbPix = WriterImage.bytesToImage( - store.getThumbnail(omero.rtypes.rint(sizeX), - omero.rtypes.rint(sizeY))); - } catch (Throwable e) { - thumbPix = null; - LogMessage msg = new LogMessage(); - msg.print("Cannot retrieve thumbnail"); - msg.print(e); - context.getLogger().error(this, msg); - } finally { - if (last) { - context.getDataService().closeService(ctx, store); - } - } - if (thumbPix == null) { - valid = false; - thumbPix = Factory.createDefaultImageThumbnail(sizeX, sizeY); - } - currentThumbnail = new ThumbnailData(imageID, thumbPix, userID, valid); - } + private Collection userIDs; /** - * Creates a {@link BatchCall} to retrieve rendering control. - * - * @return The {@link BatchCall}. + * Helper reference to the image service. */ - private BatchCall makeBatchCall() - { - return new BatchCall("Loading thumbnail for: "+pixelsID) { - public void doCall() throws Exception - { - BufferedImage thumbPix = null; - try { - thumbPix = service.getThumbnail(ctx, pixelsID, maxWidth, - maxHeight, -1); - - } catch (RenderingServiceException e) { - context.getLogger().error(this, - "Cannot retrieve thumbnail from ID: "+ - e.getExtendedMessage()); - } - if (thumbPix == null) - thumbPix = Factory.createDefaultImageThumbnail(-1); - currentThumbnail = thumbPix; - } - }; - } + private OmeroImageService service; /** - * Adds a {@link BatchCall} to the tree for each thumbnail to retrieve. - * @see BatchCallTree#buildTree() + * The security context. */ - protected void buildTree() - { - if (pixelsCall) { - add(makeBatchCall()); - return; - } - String description; - Iterator j = userIDs.iterator(); - Long id; - Iterator i; - DataObject image; - PixelsData pxd; - while (j.hasNext()) { - id = j.next(); - final long userID = id; - i = images.iterator(); - ThumbnailStorePrx store = null; - try { - store = service.createThumbnailStore(ctx); - } catch (Exception e) { - context.getLogger().debug(this, - "Cannot start thumbnail store."); - } - try { - final ThumbnailStorePrx value = store; - int size = images.size()-1; - int k = 0; - long imageID = -1; - while (i.hasNext()) { - image = (DataObject) i.next(); - if (image instanceof ImageData) { - pxd = ((ImageData) image).getDefaultPixels(); - imageID = image.getId(); - } else { - pxd = (PixelsData) image; - if (pxd != null) imageID = pxd.getImage().getId(); - } - description = "Loading thumbnail"; - final PixelsData index = pxd; - final boolean last = size == k; - k++; - final long iid = imageID; - add(new BatchCall(description) { - public void doCall() { - loadThumbail(index, userID, value, last, iid); - } - }); - } - } catch (RuntimeException r) { - // If we fail to pass control to loadThumbnail - // then we need to clean up the service. - if (store != null) { - try { - store.close(); - } catch (ServerError e) { - context.getLogger().warn(this, "Failed to close " + store); - } - } - } - } - } + private SecurityContext ctx; /** - * Returns the lastly retrieved thumbnail. - * This will be packed by the framework into a feedback event and - * sent to the provided call observer, if any. - * - * @return A {@link ThumbnailData} containing the thumbnail pixels. + * Use getConfigService() instead of this directly */ - protected Object getPartialResult() { return currentThumbnail; } + private IConfigPrx configService; /** - * Returns the last loaded thumbnail (important for the BirdsEyeLoader to - * work correctly). But in fact, thumbnails are progressively delivered with - * feedback events. - * @see BatchCallTree#getResult() + * Load the thumbnail as an full size image. */ - protected Object getResult() { return currentThumbnail; } + private boolean asImage = false; + /** * Creates a new instance. * If bad arguments are passed, we throw a runtime exception so to fail * early and in the caller's thread. - * - * @param ctx The security context. - * @param imgs Contains {@link DataObject}s, one - * for each thumbnail to retrieve. - * @param maxWidth The maximum acceptable width of the thumbnails. + * + * @param ctx The security context. + * @param imgs Contains {@link DataObject}s, one + * for each thumbnail to retrieve. + * @param maxWidth The maximum acceptable width of the thumbnails. * @param maxHeight The maximum acceptable height of the thumbnails. - * @param userIDs The users the thumbnail are for. + * @param userIDs The users the thumbnail are for. */ - public ThumbnailLoader(SecurityContext ctx, Set imgs, - int maxWidth, int maxHeight, Set userIDs) - { - if (imgs == null) throw new NullPointerException("No images."); - if (maxWidth <= 0) + public ThumbnailLoader(SecurityContext ctx, Collection imgs, + int maxWidth, int maxHeight, Collection userIDs) { + if (imgs == null) { + throw new NullPointerException("No images."); + } + + if (maxWidth <= 0) { throw new IllegalArgumentException( - "Non-positive width: "+maxWidth+"."); - if (maxHeight <= 0) + "Non-positive width: " + maxWidth + "."); + } + + if (maxHeight <= 0) { throw new IllegalArgumentException( - "Non-positive height: "+maxHeight+"."); - this.ctx = ctx; + "Non-positive height: " + maxHeight + "."); + } + + this.images = imgs; this.maxWidth = maxWidth; this.maxHeight = maxHeight; - images = imgs; this.userIDs = userIDs; - asImage = false; - service = context.getImageService(); + this.ctx = ctx; + this.service = context.getImageService(); + } + + public ThumbnailLoader(SecurityContext ctx, Collection imgs, long userID) { + this(ctx, imgs, 0, 0, Collections.singleton(userID)); + this.asImage = true; + } + + public ThumbnailLoader(SecurityContext ctx, Collection imgs, int maxWidth, int maxHeight, long userID) { + this(ctx, imgs, maxWidth, maxHeight, Collections.singleton(userID)); + } + + public ThumbnailLoader(SecurityContext ctx, ImageData image, int maxWidth, int maxHeight, long userID) { + this(ctx, new HashSet(), maxWidth, maxHeight, Collections.singleton(userID)); + images.add(image); + } + + public ThumbnailLoader(SecurityContext ctx, ImageData image, int maxWidth, int maxHeight, Collection userIDs) { + this(ctx, new HashSet(), maxWidth, maxHeight, userIDs); + images.add(image); } /** - * Creates a new instance. - * If bad arguments are passed, we throw a runtime exception so to fail - * early and in the caller's thread. - * - * @param ctx The security context. - * @param imgs Contains {@link DataObject}s, one for each thumbnail to - * retrieve. - * @param userID The user the thumbnail are for. + * Returns the last loaded thumbnail (important for the BirdsEyeLoader to + * work correctly). But in fact, thumbnails are progressively delivered with + * feedback events. + * + * @see BatchCallTree#getResult() */ - public ThumbnailLoader(SecurityContext ctx, Collection imgs, - long userID) - { - if (imgs == null) throw new NullPointerException("No images."); - this.ctx = ctx; - asImage = true; - images = imgs; - userIDs = new HashSet(1); - userIDs.add(userID); - service = context.getImageService(); + @Override + protected Object getResult() { + return currentThumbnail; } /** - * Creates a new instance. - * If bad arguments are passed, we throw a runtime exception so to fail - * early and in the caller's thread. - * - * @param ctx The security context. - * @param imgs Contains {@link DataObject}s, one for each thumbnail to - * retrieve. - * @param maxWidth The maximum acceptable width of the thumbnails. - * @param maxHeight The maximum acceptable height of the thumbnails. - * @param userID The user the thumbnail are for. + * Returns the lastly retrieved thumbnail. + * This will be packed by the framework into a feedback event and + * sent to the provided call observer, if any. + * + * @return A {@link ThumbnailData} containing the thumbnail pixels. */ - public ThumbnailLoader(SecurityContext ctx, Collection imgs, - int maxWidth, int maxHeight, long userID) - { - if (imgs == null) throw new NullPointerException("No images."); - if (maxWidth <= 0) - throw new IllegalArgumentException( - "Non-positive width: "+maxWidth+"."); - if (maxHeight <= 0) - throw new IllegalArgumentException( - "Non-positive height: "+maxHeight+"."); - this.ctx = ctx; - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - images = imgs; - userIDs = new HashSet(1); - userIDs.add(userID); - asImage = false; - service = context.getImageService(); + @Override + protected Object getPartialResult() { + return currentThumbnail; } /** - * Creates a new instance. - * If bad arguments are passed, we throw a runtime exception so to fail - * early and in the caller's thread. - * - * @param ctx The security context. - * @param image The {@link ImageData}, the thumbnail - * @param maxWidth The maximum acceptable width of the thumbnails. - * @param maxHeight The maximum acceptable height of the thumbnails. - * @param userID The user the thumbnails are for. + * Adds a {@link BatchCall} to the tree for each thumbnail to retrieve. + * + * @see BatchCallTree#buildTree() */ - public ThumbnailLoader(SecurityContext ctx, ImageData image, int maxWidth, - int maxHeight, long userID) - { - if (image == null) throw new IllegalArgumentException("No image."); - if (maxWidth <= 0) - throw new IllegalArgumentException( - "Non-positive width: "+maxWidth+"."); - if (maxHeight <= 0) - throw new IllegalArgumentException( - "Non-positive height: "+maxHeight+"."); - this.ctx = ctx; - userIDs = new HashSet(1); - userIDs.add(userID); - images = new HashSet(1); - images.add(image); - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - asImage = false; - service = context.getImageService(); + @Override + protected void buildTree() { + final int lastIndex = images.size() - 1; + for (final long userId : userIDs) { + int k = 0; + for (DataObject image : images) { + // Cast our image to pixels object + final PixelsData pxd = dataObjectToPixelsData(image); + + // Flag to check if we've iterated to the last image + final boolean last = lastIndex == k++; + + // Add a new load thumbnail task to tree + BatchCall call = new BatchCall("Loading thumbnails") { + @Override + public void doCall() throws Exception { + ThumbnailStorePrx store = getThumbnailStore(pxd); + try { + handleBatchCall(store, pxd, userId); + } finally { + if (last) { + context.getDataService() + .closeService(ctx, store); + } + } + } + }; + + add(call); + } + } + } + + private IConfigPrx getConfigService() throws DSOutOfServiceException { + if (configService == null) { + configService = context.getGateway() + .getConfigService(ctx); + } + return configService; + } + + private void handleBatchCall(ThumbnailStorePrx store, PixelsData pxd, long userId) { + // If image has pyramids, check to see if image is ready for loading as a thumbnail. + try { + Image thumbnail; + byte[] thumbnailData = loadThumbnail(store, pxd, userId); + if (thumbnailData == null || thumbnailData.length == 0) { + // Find out why the thumbnail is not ready on the server + if (requiresPixelsPyramid(pxd)) { + thumbnail = determineThumbnailState(pxd); + } else { + thumbnail = Factory.createDefaultThumbnail("Loading"); + } + } else { + thumbnail = WriterImage.bytesToImage(thumbnailData); + } + // Convert thumbnail to whatever + currentThumbnail = new ThumbnailData(pxd.getImage().getId(), + thumbnail, userId, true); + } catch (Exception e) { + context.getLogger().error(this, e.getMessage()); + } + } + + private PixelsData dataObjectToPixelsData(DataObject image) { + return image instanceof ImageData ? + ((ImageData) image).getDefaultPixels() : + (PixelsData) image; + } + + private Image determineThumbnailState(PixelsData pxd) + throws DSOutOfServiceException, ServerError { + RawPixelsStorePrx rawPixelStore = context.getGateway() + .getPixelsStore(ctx); + try { + // This method will throw if there is an issue with the pyramid + // generation (i.e. it's not finished, corrupt) + rawPixelStore.setPixelsId(pxd.getId(), false); + } catch (omero.MissingPyramidException e) { + // Thrown if pyramid file is missing, then we know the thumbnail still has + // to be generated in a short time + return Factory.createDefaultThumbnail("Loading"); + } catch (ResourceError e) { + context.getLogger().error(this, new LogMessage("Error getting pyramid from server," + + " it might be corrupt", e)); + } + return Factory.createDefaultThumbnail("Error"); + } + + private ThumbnailStorePrx getThumbnailStore(PixelsData pxd) throws DSAccessException, + DSOutOfServiceException, ServerError { + // System.out.println(image.getId()); + ThumbnailStorePrx store = service.createThumbnailStore(ctx); + if (!store.setPixelsId(pxd.getId())) { + store.resetDefaults(); + store.setPixelsId(pxd.getId()); + } + return store; } /** - * Creates a new instance. - * If bad arguments are passed, we throw a runtime exception so to fail - * early and in the caller's thread. - * - * @param ctx The security context. - * @param pixelsID The id of the pixel set. - * @param maxWidth The m aximum acceptable width of the thumbnails. - * @param maxHeight The maximum acceptable height of the thumbnails. - * @param userID The user the thumbnail are for. + * Loads the thumbnail for {@link #images}[index]. + * + * @param pxd The image the thumbnail for. + * @param userId The id of the user the thumbnail is for. + * @param store The thumbnail store to use. */ - public ThumbnailLoader(SecurityContext ctx, long pixelsID, int maxWidth, - int maxHeight, long userID) - { - if (maxWidth <= 0) - throw new IllegalArgumentException( - "Non-positive id: "+pixelsID+"."); - if (maxWidth <= 0) - throw new IllegalArgumentException( - "Non-positive width: "+maxWidth+"."); - if (maxHeight <= 0) - throw new IllegalArgumentException( - "Non-positive height: "+maxHeight+"."); - this.ctx = ctx; - pixelsCall = true; - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.pixelsID = pixelsID; - userIDs = new HashSet(1); - userIDs.add(userID); - service = context.getImageService(); + private byte[] loadThumbnail(ThumbnailStorePrx store, PixelsData pxd, long userId) + throws ServerError, DSAccessException, DSOutOfServiceException { + int sizeX = maxWidth, sizeY = maxHeight; + if (asImage) { + sizeX = pxd.getSizeX(); + sizeY = pxd.getSizeY(); + } else { + Dimension d = Factory.computeThumbnailSize(sizeX, sizeY, + pxd.getSizeX(), pxd.getSizeY()); + sizeX = d.width; + sizeY = d.height; + } + + if (userId >= 0) { + long rndDefId = service.getRenderingDef(ctx, + pxd.getId(), userId); + // the user might not have own rendering settings + // for this image + if (rndDefId >= 0) + store.setRenderingDefId(rndDefId); + } + + if (VersionCompare.compare(context.getGateway().getServerVersion(), VERSION_THUMBNAIL_NO_DEFAULT) >= 0) { + // If the client is connecting to a server with version 5.4.8 or greater, use the thumbnail + // loading function that doesn't return a clock. + return store.getThumbnailWithoutDefault(omero.rtypes.rint(sizeX), + omero.rtypes.rint(sizeY)); + } else { + return store.getThumbnail(omero.rtypes.rint(sizeX), + omero.rtypes.rint(sizeY)); + } } /** - * Creates a new instance. - * If bad arguments are passed, we throw a runtime exception so to fail - * early and in the caller's thread. - * - * @param ctx The security context. - * @param image The {@link ImageData}, the thumbnail - * @param maxWidth The maximum acceptable width of the thumbnails. - * @param maxHeight The maximum acceptable height of the thumbnails. - * @param userIDs The users the thumbnail are for. + * Returns whether a pyramid should be used for the given {@link PixelsData}. + * This usually implies that this is a "Big image" and therefore will + * need tiling. + * + * @param pxd + * @return */ - public ThumbnailLoader(SecurityContext ctx, ImageData image, int maxWidth, - int maxHeight, Set userIDs) - { - if (image == null) throw new IllegalArgumentException("No image."); - if (maxWidth <= 0) - throw new IllegalArgumentException( - "Non-positive width: "+maxWidth+"."); - if (maxHeight <= 0) - throw new IllegalArgumentException( - "Non-positive height: "+maxHeight+"."); - this.ctx = ctx; - images = new HashSet(1); - images.add(image); - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.userIDs = userIDs; - asImage = false; - service = context.getImageService(); + private boolean requiresPixelsPyramid(PixelsData pxd) throws ServerError, DSOutOfServiceException { + int maxWidth = Integer.parseInt(getConfigService() + .getConfigValue("omero.pixeldata.max_plane_width")); + int maxHeight = Integer.parseInt(getConfigService() + .getConfigValue("omero.pixeldata.max_plane_height")); + return pxd.getSizeX() * pxd.getSizeY() > maxWidth * maxHeight; } } diff --git a/components/insight/SRC/org/openmicroscopy/shoola/util/VersionCompare.java b/components/insight/SRC/org/openmicroscopy/shoola/util/VersionCompare.java new file mode 100644 index 00000000000..ccbc5f970ef --- /dev/null +++ b/components/insight/SRC/org/openmicroscopy/shoola/util/VersionCompare.java @@ -0,0 +1,37 @@ +package org.openmicroscopy.shoola.util; + +public class VersionCompare { + + /** + * Compares two version strings. Source + *

+ * Use this instead of String.compareTo() for a non-lexicographical + * comparison that works for version strings. e.g. "1.10".compareTo("1.6"). + *

+ * Note: It does not work if "1.10" is supposed to be equal to "1.10.0". + * + * @param str1 a string of ordinal numbers separated by decimal points. + * @param str2 a string of ordinal numbers separated by decimal points. + * @return The result is a negative integer if str1 is _numerically_ less than str2. + * The result is a positive integer if str1 is _numerically_ greater than str2. + * The result is zero if the strings are _numerically_ equal. + */ + public static int compare(String str1, String str2) { + String[] vals1 = str1.split("\\."); + String[] vals2 = str2.split("\\."); + int i = 0; + // set index to first non-equal ordinal or length of shortest version string + while (i < vals1.length && i < vals2.length && vals1[i].equals(vals2[i])) { + i++; + } + // compare first non-equal ordinal number + if (i < vals1.length && i < vals2.length) { + int diff = Integer.valueOf(vals1[i]).compareTo(Integer.valueOf(vals2[i])); + return Integer.signum(diff); + } + // the strings are equal or one string is a substring of the other + // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" + return Integer.signum(vals1.length - vals2.length); + } + +} diff --git a/components/server/src/ome/services/ThumbnailBean.java b/components/server/src/ome/services/ThumbnailBean.java index 3cd73336431..8007fc05f90 100644 --- a/components/server/src/ome/services/ThumbnailBean.java +++ b/components/server/src/ome/services/ThumbnailBean.java @@ -481,9 +481,8 @@ public void setSettingsService(IRenderingSettings settingsService) { * @throws IOException * if there is a problem writing to disk. */ - private void compressThumbnailToDisk(Thumbnail thumb, BufferedImage image) - throws IOException { - + private void compressThumbnailToDisk(Thumbnail thumb, BufferedImage image, boolean inProgress) + throws IOException { if (diskSpaceChecking) { iRepositoryInfo.sanityCheckRepository(); } @@ -491,7 +490,7 @@ private void compressThumbnailToDisk(Thumbnail thumb, BufferedImage image) FileOutputStream stream = ioService.getThumbnailOutputStream(thumb); try { if (inProgress) { - compressInProgressImageToStream(thumb, stream); + compressInProgressImageToStream(thumb, stream, inProgressImageResource); } else { compressionService.compressToStream(image, stream); } @@ -502,45 +501,48 @@ private void compressThumbnailToDisk(Thumbnail thumb, BufferedImage image) /** * Compresses the in progress image to a stream. - * @param thumb The thumbnail metadata. + * + * @param thumb Thumbnail meta data * @param outputStream Stream to compress the data to. + * @param inProgressImageResource The image file (located in resources) to write to disk + * @throws IOException */ - private void compressInProgressImageToStream( - Thumbnail thumb, OutputStream outputStream) { - int x = thumb.getSizeX(); - int y = thumb.getSizeY(); - StopWatch s1 = new Slf4JStopWatch("omero.transcodeSVG"); - try - { - SVGRasterizer rasterizer = new SVGRasterizer( - inProgressImageResource.getInputStream()); - // Batik will automatically maintain the aspect ratio of the - // resulting image if we only specify the width or height. - if (x > y) - { - rasterizer.setImageWidth(x); - } - else - { - rasterizer.setImageHeight(y); - } - rasterizer.setQuality(compressionService.getCompressionLevel()); - rasterizer.createJPEG(outputStream); - s1.stop(); - } - catch (IOException e1) - { - String s = "Error loading in-progress image from Spring resource."; - log.error(s, e1); - throw new ResourceError(s); - } - catch (TranscoderException e2) - { - String s = "Error transcoding in progress SVG."; - log.error(s, e2); - throw new ResourceError(s); - } - } + private void compressInProgressImageToStream(Thumbnail thumb, OutputStream outputStream, + Resource inProgressImageResource) { + int x = thumb.getSizeX(); + int y = thumb.getSizeY(); + StopWatch s1 = new Slf4JStopWatch("omero.transcodeSVG"); + try + { + SVGRasterizer rasterizer = new SVGRasterizer( + inProgressImageResource.getInputStream()); + // Batik will automatically maintain the aspect ratio of the + // resulting image if we only specify the width or height. + if (x > y) + { + rasterizer.setImageWidth(x); + } + else + { + rasterizer.setImageHeight(y); + } + rasterizer.setQuality(compressionService.getCompressionLevel()); + rasterizer.createJPEG(outputStream); + s1.stop(); + } + catch (IOException e1) + { + String s = "Error loading in-progress image from Spring resource."; + log.error(s, e1); + throw new ResourceError(s); + } + catch (TranscoderException e2) + { + String s = "Error transcoding in progress SVG."; + log.error(s, e2); + throw new ResourceError(s); + } + } /** * Checks that sizeX and sizeY are not out of range for the active pixels @@ -819,10 +821,26 @@ public void createThumbnail(Integer sizeX, Integer sizeY) } } - /** Actually does the work specified by {@link #createThumbnail(Integer, Integer)}. */ + /** + * Calls {@code _createThumbnail(Thumbnail thumbMetaData)} with the local + * variable {@code thumbnailMetadata} as the input parameter. + * + * @return thumbnail object + */ private Thumbnail _createThumbnail() { + // For old times sake + return _createThumbnail(thumbnailMetadata); + } + + /** + * Actually does the work specified by {@link #createThumbnail(Integer, Integer)}. + * + * @param thumbMetaData Thumbnail meta data object + * @return + */ + private Thumbnail _createThumbnail(Thumbnail thumbMetaData) { StopWatch s1 = new Slf4JStopWatch("omero._createThumbnail"); - if (thumbnailMetadata == null) { + if (thumbMetaData == null) { throw new ValidationException("Missing thumbnail metadata."); } else if (ctx.dirtyMetadata(pixels.getId())) { // Increment the version of the thumbnail so that its @@ -831,7 +849,7 @@ private Thumbnail _createThumbnail() { // implemented using IUpdate.touch() or similar once that // functionality exists. //Check first if the thumbnail is the one of the settings owner - Long ownerId = thumbnailMetadata.getDetails().getOwner().getId(); + Long ownerId = thumbMetaData.getDetails().getOwner().getId(); Long rndOwnerId = settings.getDetails().getOwner().getId(); final Long rndGroupId = settings.getDetails().getGroup().getId(); final Map groupContext = new HashMap<>(); @@ -846,16 +864,16 @@ private Thumbnail _createThumbnail() { } if (rndOwnerId.equals(ownerId)) { final Pixels unloadedPixels = new Pixels(pixels.getId(), false); - thumbnailMetadata.setPixels(unloadedPixels); - _setMetadataVersion(thumbnailMetadata, inProgress); + thumbMetaData.setPixels(unloadedPixels); + _setMetadataVersion(thumbMetaData, inProgress); dirtyMetadata = true; } else { //new one for owner of the settings. - final Dimension d = new Dimension(thumbnailMetadata.getSizeX(), - thumbnailMetadata.getSizeY()); - thumbnailMetadata = ctx.createThumbnailMetadata(pixels, d); - _setMetadataVersion(thumbnailMetadata, inProgress); - thumbnailMetadata = iUpdate.saveAndReturnObject(thumbnailMetadata); + final Dimension d = new Dimension(thumbMetaData.getSizeX(), + thumbMetaData.getSizeY()); + thumbMetaData = ctx.createThumbnailMetadata(pixels, d); + _setMetadataVersion(thumbMetaData, inProgress); + thumbMetaData = iUpdate.saveAndReturnObject(thumbMetaData); dirtyMetadata = false; } } finally { @@ -874,9 +892,9 @@ private Thumbnail _createThumbnail() { BufferedImage image = createScaledImage(null, null); try { - compressThumbnailToDisk(thumbnailMetadata, image); + compressThumbnailToDisk(thumbMetaData, image, inProgress); s1.stop(); - return thumbnailMetadata; + return thumbMetaData; } catch (IOException e) { log.error("Thumbnail could not be compressed.", e); throw new ResourceError(e.getMessage()); @@ -1070,6 +1088,35 @@ public byte[] getThumbnail(Integer sizeX, Integer sizeY) { return value; } + /* + * (non-Javadoc) + * + * @see ome.api.ThumbnailStore#getThumbnail(ome.model.core.Pixels, + * ome.model.displayRenderingDef, java.lang.Integer, + * java.lang.Integer) + */ + @RolesAllowed("user") + @Transactional(readOnly = false) + public byte[] getThumbnailWithoutDefault(Integer sizeX, Integer sizeY) { + errorIfNullPixelsAndRenderingDef(); + Dimension dimensions = sanityCheckThumbnailSizes(sizeX, sizeY); + Set pixelsIds = Collections.singleton(pixelsId); + ctx.loadAndPrepareMetadata(pixelsIds, dimensions); + + // If this comes back null, don't have a thumbnail yet + thumbnailMetadata = ctx.getMetadataSimple(pixelsId); + if (thumbnailMetadata == null) { + // We don't have a thumbnail to load, lets try to create it + // and then return it + thumbnailMetadata = ctx.createThumbnailMetadata(pixels, dimensions); + } + + byte[] value = retrieveThumbnail(thumbnailMetadata); + // I don't really know why this is here, no iquery calls being that I can see... + iQuery.clear();//see #11072 + return value; + } + /** * Creates the thumbnail or retrieves it from cache and updates the * thumbnail metadata. @@ -1159,6 +1206,49 @@ private byte[] retrieveThumbnail(boolean rewriteMetadata) } } + /** + * A simple way to creates the thumbnail or retrieves it from cache. + * + * @return Thumbnail bytes. + */ + private byte[] retrieveThumbnail(Thumbnail thumbMetaData) throws ResourceError { + try { + return ioService.getThumbnail(thumbMetaData); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Cache miss, thumbnail missing or out of date."); + } + } + + final long pixelsId = thumbMetaData.getPixels().getId(); + if (!ctx.hasSettings(pixelsId)) { + try { + pixelDataService.getPixelBuffer(ctx.getPixels(pixelsId), false); + } catch (ConcurrencyException ce) { + return new byte[0]; + } + } + + // If we get here, then we can assume the thumbnail just needs created + // and saved to disk + try { + BufferedImage image = createScaledImage(null, null); + if (image != null) { + compressThumbnailToDisk(thumbMetaData, image, false); + } + } catch (Exception e) { + log.error("Thumbnail could not be compressed.", e); + throw new ResourceError(e.getMessage()); + } + + try { + return ioService.getThumbnail(thumbMetaData); + } catch (IOException e) { + log.error("Could not obtain thumbnail", e); + throw new ResourceError(e.getMessage()); + } + } + /* * (non-Javadoc) * @@ -1227,11 +1317,11 @@ private byte[] retrieveThumbnailDirect(Integer sizeX, Integer sizeY, thumbnailMetadata = local; } - BufferedImage image = createScaledImage(theZ, theT); + BufferedImage image = inProgress? null : createScaledImage(theZ, theT); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); try { if (inProgress) { - compressInProgressImageToStream(local, byteStream); + compressInProgressImageToStream(local, byteStream, inProgressImageResource); } else { compressionService.compressToStream(image, byteStream); } @@ -1423,4 +1513,4 @@ private byte[] handleNoThumbnail(Throwable t, Dimension dimensions) { } } -} +} \ No newline at end of file diff --git a/components/server/src/ome/services/ThumbnailCtx.java b/components/server/src/ome/services/ThumbnailCtx.java index 328295e8c11..dfa8fa4ccfc 100644 --- a/components/server/src/ome/services/ThumbnailCtx.java +++ b/components/server/src/ome/services/ThumbnailCtx.java @@ -494,6 +494,25 @@ else if (thumbnail == null) return thumbnail; } + /** + * Retrieves the Thumbnail object for a given Pixels ID. + * + * @param pixelsId Pixels ID to retrieve the Thumbnail object for. + * @return returns null if the thumbnail metadata can't be found + */ + public Thumbnail getMetadataSimple(long pixelsId) throws ResourceError { + Thumbnail thumbnail = pixelsIdMetadataMap.get(pixelsId); + if (thumbnail == null && securitySystem.isGraphCritical(null)) { + 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)); + } + return thumbnail; + } + /** * Whether or not the thumbnail metadata for a given Pixels ID is dirty * (the RenderingDef has been updated since the Thumbnail was).