Java tutorial
/* * $Id$ * * Copyright 2006 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.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; 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 java.util.concurrent.locks.ReentrantReadWriteLock; import ome.annotations.RolesAllowed; import ome.api.IPixels; import ome.api.IRenderingSettings; import ome.api.IRepositoryInfo; import ome.api.IScale; import ome.api.ServiceInterface; import ome.api.ThumbnailStore; import ome.api.local.LocalCompress; import ome.conditions.ApiUsageException; import ome.conditions.ConcurrencyException; import ome.conditions.InternalException; import ome.conditions.ResourceError; import ome.conditions.ValidationException; import ome.io.nio.PixelBuffer; import ome.io.nio.PixelsService; import ome.io.nio.ThumbnailService; import ome.logic.AbstractLevel2Service; import ome.model.core.Pixels; import ome.model.display.RenderingDef; import ome.model.display.Thumbnail; import ome.model.enums.Family; import ome.model.enums.RenderingModel; import ome.model.meta.Session; import ome.parameters.Parameters; import ome.system.EventContext; import ome.system.SimpleEventContext; import ome.util.ImageUtil; import omeis.providers.re.Renderer; import omeis.providers.re.data.PlaneDef; import omeis.providers.re.quantum.QuantizationException; import omeis.providers.re.quantum.QuantumFactory; import org.apache.batik.transcoder.TranscoderException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.perf4j.StopWatch; import org.perf4j.commonslog.CommonsLogStopWatch; import org.springframework.core.io.Resource; import org.springframework.transaction.annotation.Transactional; /** * Provides methods for directly querying object graphs. The service is entirely * read/write transactionally because of the requirements of rendering engine * lazy object creation where rendering settings are missing. * * @author Chris Allan <a * href="mailto:callan@blackcat.ca">callan@blackcat.ca</a> * @version 3.0 <small> (<b>Internal version:</b> $Rev$ $Date$) </small> * @since 3.0 * */ @Transactional(readOnly = true) public class ThumbnailBean extends AbstractLevel2Service implements ThumbnailStore, Serializable { /** * */ private static final long serialVersionUID = 3047482880497900069L; /** The logger for this class. */ private transient static Log log = LogFactory.getLog(ThumbnailBean.class); /** The renderer that this service uses for thumbnail creation. */ private transient Renderer renderer; /** The scaling service will be used to scale buffered images. */ private transient IScale iScale; /** The pixels service, will be used to load pixels and settings. */ private transient IPixels iPixels; /** The service used to retrieve the pixels data. */ private transient PixelsService pixelDataService; /** The ROMIO thumbnail service. */ private transient ThumbnailService ioService; /** The disk space checking service. */ private transient IRepositoryInfo iRepositoryInfo; /** The JPEG compression service. */ private transient LocalCompress compressionService; /** The rendering settings service. */ private transient IRenderingSettings settingsService; /** The list of all families supported by the {@link Renderer}. */ private transient List<Family> families; /** The list of all rendering models supported by the {@link Renderer}. */ private transient List<RenderingModel> renderingModels; /** If the file service checking for disk overflow. */ private transient boolean diskSpaceChecking; /** If the renderer is dirty. */ private Boolean dirty = true; /** If the settings {@link metadata} is dirty. */ private Boolean dirtyMetadata = false; /** The pixels instance that the service is currently working on. */ private Pixels pixels; /** ID of the pixels instance that the service is currently working on. */ private Long pixelsId; /** In progress marker; set to true when no data is available the pixel */ private boolean inProgress; /** The rendering settings that the service is currently working with. */ private RenderingDef settings; /** The thumbnail metadata that the service is currently working with. */ private Thumbnail thumbnailMetadata; /** The thumbnail metadata context. */ private ThumbnailCtx ctx; /** The in-progress image resource we'll use for in progress images. */ private Resource inProgressImageResource; /** The default X-width for a thumbnail. */ public static final int DEFAULT_X_WIDTH = 48; /** The default Y-width for a thumbnail. */ public static final int DEFAULT_Y_WIDTH = 48; /** The default compression quality in fractional percent. */ public static final float DEFAULT_COMPRESSION_QUALITY = 0.85F; /** The default MIME type. */ public static final String DEFAULT_MIME_TYPE = "image/jpeg"; /** * read-write lock to prevent READ-calls during WRITE operations. * * It is safe for the lock to be serialized. On deserialization, it will * be in the unlocked state. */ private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); /** Notification that the bean has just returned from passivation. */ private transient boolean wasPassivated = false; /** default constructor */ public ThumbnailBean() { } /** * overriden to allow Spring to set boolean * @param checking */ public ThumbnailBean(boolean checking) { this.diskSpaceChecking = checking; } public Class<? extends ServiceInterface> getServiceInterface() { return ThumbnailStore.class; } // ~ Lifecycle methods // ========================================================================= // See documentation on JobBean#passivate @RolesAllowed("user") @Transactional(readOnly = true) public void passivate() { log.debug("***** Passivating... ******"); rwl.writeLock().lock(); try { if (renderer != null) { renderer.close(); } renderer = null; } finally { rwl.writeLock().unlock(); } } // See documentation on JobBean#activate @RolesAllowed("user") @Transactional(readOnly = true) public void activate() { log.debug("***** Returning from passivation... ******"); rwl.writeLock().lock(); try { wasPassivated = true; } finally { rwl.writeLock().unlock(); } } @RolesAllowed("user") public void close() { rwl.writeLock().lock(); log.debug("Closing thumbnail bean"); try { if (renderer != null) { renderer.close(); } ctx = null; settings = null; pixels = null; thumbnailMetadata = null; renderer = null; iScale = null; ioService = null; } finally { rwl.writeLock().unlock(); } } @RolesAllowed("user") public long getRenderingDefId() { if (settings == null || settings.getId() == null) { throw new ApiUsageException("No rendering def"); } return settings.getId(); } /* * (non-Javadoc) * * @see ome.api.StatefulServiceInterface#getCurrentEventContext() */ public EventContext getCurrentEventContext() { return new SimpleEventContext(getSecuritySystem().getEventContext()); } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#setPixelsId(long) */ @RolesAllowed("user") @Transactional(readOnly = false) public boolean setPixelsId(long id) { // If we've had a pixels set change, reset our stateful objects. if ((pixels != null && pixels.getId() != id) || pixels == null) { resetMetadata(); newContext(); } Set<Long> pixelsIds = new HashSet<Long>(); pixelsIds.add(id); ctx.loadAndPrepareRenderingSettings(pixelsIds); pixels = ctx.getPixels(id); pixelsId = pixels.getId(); settings = ctx.getSettings(id); if (ctx.hasSettings(id)) { return true; } return false; } /** * Retrieves a list of the families supported by the {@link Renderer} * either from instance variable cache or the database. * @return See above. */ private List<Family> getFamilies() { if (families == null) { families = iPixels.getAllEnumerations(Family.class); } return families; } /** * Retrieves a list of the rendering models supported by the * {@link Renderer} either from instance variable cache or the database. * @return See above. */ private List<RenderingModel> getRenderingModels() { if (renderingModels == null) { renderingModels = iPixels.getAllEnumerations(RenderingModel.class); } return renderingModels; } /** * Retrieves a deep copy of the pixels set and rendering settings as * required for a rendering event and creates a renderer. This method * should only be called if a rendering event is required. */ private void load() { if (renderer != null) { renderer.close(); } pixels = iPixels.retrievePixDescription(pixels.getId()); settings = iPixels.loadRndSettings(settings.getId()); List<Family> families = getFamilies(); List<RenderingModel> renderingModels = getRenderingModels(); QuantumFactory quantumFactory = new QuantumFactory(families); // Loading last to try to ensure that the buffer will get closed. PixelBuffer buffer = pixelDataService.getPixelBuffer(pixels, false); renderer = new Renderer(quantumFactory, renderingModels, pixels, settings, buffer); dirty = false; } /* (non-Javadoc) * @see ome.api.ThumbnailStore#setRenderingDefId(java.lang.Long) */ @RolesAllowed("user") public void setRenderingDefId(long id) { errorIfNullPixels(); ctx.loadAndPrepareRenderingSettings(pixelsId, id); settings = ctx.getSettings(pixelsId); // Handle cases where this new settings is not owned by us so that // retrieval of thumbnail metadata is done based on the owner of the // settings not the owner of the session. (#2274 Part I) ctx.setUserId(settings.getDetails().getOwner().getId()); } /** * In-progress image resource Bean injector. * @param inProgressImageResource The in-progress image resource we'll be * using for in progress images. */ public void setInProgressImageResource(Resource inProgressImageResource) { getBeanHelper().throwIfAlreadySet(this.inProgressImageResource, inProgressImageResource); this.inProgressImageResource = inProgressImageResource; } /** * Pixels service Bean injector. * * @param iPixels * an <code>IPixels</code>. */ public void setPixelDataService(PixelsService pixelDataService) { getBeanHelper().throwIfAlreadySet(this.pixelDataService, pixelDataService); this.pixelDataService = pixelDataService; } /** * Pixels service Bean injector. * * @param iPixels * an <code>IPixels</code>. */ public void setIPixels(IPixels iPixels) { getBeanHelper().throwIfAlreadySet(this.iPixels, iPixels); this.iPixels = iPixels; } /** * Scale service Bean injector. * * @param iScale * an <code>IScale</code>. */ public void setScaleService(IScale iScale) { getBeanHelper().throwIfAlreadySet(this.iScale, iScale); this.iScale = iScale; } /** * I/O service (ThumbnailService) Bean injector. * * @param ioService * a <code>ThumbnailService</code>. */ public void setIoService(ThumbnailService ioService) { getBeanHelper().throwIfAlreadySet(this.ioService, ioService); this.ioService = ioService; } /** * Disk Space Usage service Bean injector * @param iRepositoryInfo * an <code>IRepositoryInfo</code> */ public final void setIRepositoryInfo(IRepositoryInfo iRepositoryInfo) { getBeanHelper().throwIfAlreadySet(this.iRepositoryInfo, iRepositoryInfo); this.iRepositoryInfo = iRepositoryInfo; } /** * Compression service Bean injector. * * @param compressionService * an <code>ICompress</code>. */ public void setCompressionService(LocalCompress compressionService) { getBeanHelper().throwIfAlreadySet(this.compressionService, compressionService); this.compressionService = compressionService; } /** * Rendering settings service Bean injector. * * @param settingsService * an <code>IRenderingSettings</code>. */ public void setSettingsService(IRenderingSettings settingsService) { getBeanHelper().throwIfAlreadySet(this.settingsService, settingsService); this.settingsService = settingsService; } /** * Compresses a buffered image thumbnail to disk. * * @param thumb * the thumbnail metadata. * @param image * the thumbnail's buffered image. * @throws IOException * if there is a problem writing to disk. */ private void compressThumbnailToDisk(Thumbnail thumb, BufferedImage image) throws IOException { if (diskSpaceChecking) { iRepositoryInfo.sanityCheckRepository(); } FileOutputStream stream = ioService.getThumbnailOutputStream(thumb); try { if (inProgress) { compressInProgressImageToStream(thumb, stream); } else { compressionService.compressToStream(image, stream); } } finally { stream.close(); } } /** * Compresses the <i>in progress</i> image to a stream. * @param thumb The thumbnail metadata. * @param outputStream Stream to compress the data to. */ private void compressInProgressImageToStream(Thumbnail thumb, OutputStream outputStream) { int x = thumb.getSizeX(); int y = thumb.getSizeY(); StopWatch s1 = new CommonsLogStopWatch("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); } } /** * Returns the Id of the currently logged in user. * Returns owner of the share while in share * @return See above. */ private Long getCurrentUserId() { Long shareId = getSecuritySystem().getEventContext().getCurrentShareId(); if (shareId != null) { Session s = iQuery.get(Session.class, shareId); return s.getOwner().getId(); } return getSecuritySystem().getEventContext().getCurrentUserId(); } /** * Checks that sizeX and sizeY are not out of range for the active pixels * set and returns a set of valid dimensions. * * @param sizeX * the X-width for the requested thumbnail. * @param sizeY * the Y-width for the requested thumbnail. * @return A set of valid XY dimensions. */ private Dimension sanityCheckThumbnailSizes(Integer sizeX, Integer sizeY) { // Sanity checks if (sizeX == null) { sizeX = DEFAULT_X_WIDTH; } if (sizeX < 0) { throw new ApiUsageException("sizeX is negative"); } if (sizeY == null) { sizeY = DEFAULT_Y_WIDTH; } if (sizeY < 0) { throw new ApiUsageException("sizeY is negative"); } return new Dimension(sizeX, sizeY); } /** * Creates a scaled buffered image from the active pixels set. * * @param def * the rendering settings to use for buffered image creation. * @param theZ the optical section (offset across the Z-axis) requested. * <pre>null</pre> signifies the rendering engine default. * @param theT the timepoint (offset across the T-axis) requested. * <pre>null</pre> signifies the rendering engine default. * @return a scaled buffered image. */ private BufferedImage createScaledImage(Integer theZ, Integer theT) { // Ensure that we have a valid state for rendering errorIfInvalidState(); if (inProgress) { return null; } // Retrieve our rendered data if (theZ == null) theZ = settings.getDefaultZ(); if (theT == null) theT = settings.getDefaultT(); PlaneDef pd = new PlaneDef(PlaneDef.XY, theT); pd.setZ(theZ); // Use a resolution level that matches our requested size if we can PixelBuffer pixelBuffer = renderer.getPixels(); int originalSizeX = pixels.getSizeX(); int originalSizeY = pixels.getSizeY(); int pixelBufferSizeX = pixelBuffer.getSizeX(); int pixelBufferSizeY = pixelBuffer.getSizeY(); if (pixelBuffer.getResolutionLevels() > 1) { int resolutionLevel = pixelBuffer.getResolutionLevels(); while (resolutionLevel > 0) { resolutionLevel--; renderer.setResolutionLevel(resolutionLevel); pixelBufferSizeX = pixelBuffer.getSizeX(); pixelBufferSizeY = pixelBuffer.getSizeY(); if (pixelBufferSizeX <= thumbnailMetadata.getSizeX() || pixelBufferSizeY <= thumbnailMetadata.getSizeY()) { break; } } log.debug(String.format("Using resolution level %d -- %dx%d", resolutionLevel, pixelBufferSizeX, pixelBufferSizeY)); renderer.setResolutionLevel(resolutionLevel); } // Render the planes and translate to a buffered image Pixels rendererPixels = renderer.getMetadata(); try { log.debug( String.format("Setting renderer Pixel sizeX:%d sizeY:%d", pixelBufferSizeX, pixelBufferSizeY)); rendererPixels.setSizeX(pixelBufferSizeX); rendererPixels.setSizeY(pixelBufferSizeY); int[] buf = renderer.renderAsPackedInt(pd, null); BufferedImage image = ImageUtil.createBufferedImage(buf, pixelBufferSizeX, pixelBufferSizeY); // Finally, scale our image using scaling factors (percentage). float xScale = (float) thumbnailMetadata.getSizeX() / pixelBufferSizeX; float yScale = (float) thumbnailMetadata.getSizeY() / pixelBufferSizeY; log.debug(String.format("Using scaling factors x:%f y:%f", xScale, yScale)); return iScale.scaleBufferedImage(image, xScale, yScale); } catch (IOException e) { ResourceError re = new ResourceError("IO error while rendering: " + e.getMessage()); re.initCause(e); throw re; } catch (QuantizationException e) { InternalException ie = new InternalException( "QuantizationException while rendering: " + e.getMessage()); ie.initCause(e); throw ie; } finally { // Reset to our original dimensions (#5075) log.debug(String.format("Setting original renderer Pixel sizeX:%d sizeY:%d", originalSizeX, originalSizeY)); rendererPixels.setSizeX(originalSizeX); rendererPixels.setSizeY(originalSizeY); } } /** * Creates a new thumbnail context. */ private void newContext() { resetMetadata(); ctx = new ThumbnailCtx(iQuery, iUpdate, iPixels, settingsService, ioService, sec, getCurrentUserId()); } /** * Resets the current metadata state. */ private void resetMetadata() { inProgress = false; pixels = null; pixelsId = null; settings = null; dirty = true; dirtyMetadata = false; thumbnailMetadata = null; // Be as explicit as possible when closing the renderer to try and // avoid re-use where we don't want it. (#2075 and #2274 Part II) if (renderer != null) { renderer.close(); } renderer = null; } protected void errorIfInvalidState() { errorIfNullPixelsAndRenderingDef(); if (inProgress) { return; // No-op #5191 } if ((renderer == null && wasPassivated) || dirty) { try { load(); } catch (ConcurrencyException e) { inProgress = true; log.info("ConcurrencyException on load()"); } } else if (renderer == null) { throw new InternalException("Thumbnail service state corruption: Renderer missing."); } } protected void errorIfNullPixelsAndRenderingDef() { errorIfNullPixels(); errorIfNullRenderingDef(); } protected void errorIfNullPixels() { if (pixels == null) { throw new ApiUsageException("Thumbnail service not ready: Pixels not set."); } } protected void errorIfNullRenderingDef() { errorIfNullPixels(); if (inProgress) { // pass. Do nothing. } else if (settings == null && ctx.isExtendedGraphCritical(Collections.singleton(pixelsId))) { long ownerId = pixels.getDetails().getOwner().getId(); throw new ResourceError(String.format( "The owner id:%d has not viewed the Pixels set id:%d, " + "rendering settings are missing.", ownerId, pixelsId)); } else if (settings == null) { throw new ome.conditions.InternalException("Fatal error retrieving rendering settings or settings " + "not loaded for Pixels set id:" + pixelsId); } } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#createThumbnail(ome.model.core.Pixels, * ome.model.display.RenderingDef, java.lang.Integer, * java.lang.Integer) */ @RolesAllowed("user") @Transactional(readOnly = false) public void createThumbnail(Integer sizeX, Integer sizeY) { if (inProgress) { return; } try { // Set defaults and sanity check thumbnail sizes if (sizeX == null) { sizeX = DEFAULT_X_WIDTH; } if (sizeY == null) { sizeY = DEFAULT_Y_WIDTH; } Dimension dimensions = sanityCheckThumbnailSizes(sizeX, sizeY); Set<Long> pixelsIds = new HashSet<Long>(); pixelsIds.add(pixelsId); ctx.loadAndPrepareMetadata(pixelsIds, dimensions); thumbnailMetadata = ctx.getMetadata(pixels.getId()); thumbnailMetadata = _createThumbnail(); if (dirtyMetadata) { thumbnailMetadata = iUpdate.saveAndReturnObject(thumbnailMetadata); } // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); } finally { dirtyMetadata = false; } } /** Actually does the work specified by {@link createThumbnail()}.*/ private Thumbnail _createThumbnail() { StopWatch s1 = new CommonsLogStopWatch("omero._createThumbnail"); if (thumbnailMetadata == null) { throw new ValidationException("Missing thumbnail metadata."); } else if (ctx.dirtyMetadata(pixels.getId())) { // Increment the version of the thumbnail so that its // update event has a timestamp equal to or after that of // the rendering settings. FIXME: This should be // implemented using IUpdate.touch() or similar once that // functionality exists. thumbnailMetadata.setVersion(thumbnailMetadata.getVersion() + 1); Pixels unloadedPixels = new Pixels(pixels.getId(), false); thumbnailMetadata.setPixels(unloadedPixels); dirtyMetadata = true; } // dirtyMetadata is left false here because we may be creating a // thumbnail for the first time and the Thumbnail object has just been // created upstream of us. BufferedImage image = createScaledImage(null, null); try { compressThumbnailToDisk(thumbnailMetadata, image); s1.stop(); return thumbnailMetadata; } catch (IOException e) { log.error("Thumbnail could not be compressed.", e); throw new ResourceError(e.getMessage()); } } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#createThumbnails(ome.model.core.Pixels, * ome.model.display.RenderingDef) */ @RolesAllowed("user") @Transactional(readOnly = false) public void createThumbnails() { try { List<Thumbnail> thumbnails = ctx.loadAllMetadata(pixelsId); for (Thumbnail thumbnail : thumbnails) { thumbnailMetadata = thumbnail; _createThumbnail(); } // We're doing the update or creation and save as a two step // process due to the possible unloaded Pixels. If we do not, // Pixels will be unloaded and we will hit // IllegalStateException's when checking update events. iUpdate.saveArray(thumbnails.toArray(new Thumbnail[thumbnails.size()])); // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); } finally { dirtyMetadata = false; } } @RolesAllowed("user") @Transactional(readOnly = false) public void createThumbnailsByLongestSideSet(Integer size, Set<Long> pixelsIds) { getThumbnailByLongestSideSet(size, pixelsIds); } /* (non-Javadoc) * @see ome.api.ThumbnailStore#getThumbnailSet(java.lang.Integer, java.lang.Integer, java.util.Set) */ @RolesAllowed("user") @Transactional(readOnly = false) public Map<Long, byte[]> getThumbnailSet(Integer sizeX, Integer sizeY, Set<Long> pixelsIds) { // Set defaults and sanity check thumbnail sizes Dimension checkedDimensions = sanityCheckThumbnailSizes(sizeX, sizeY); // Prepare our thumbnail context newContext(); ctx.loadAndPrepareRenderingSettings(pixelsIds); ctx.createAndPrepareMissingRenderingSettings(pixelsIds); ctx.loadAndPrepareMetadata(pixelsIds, checkedDimensions); return retrieveThumbnailSet(pixelsIds); } @RolesAllowed("user") @Transactional(readOnly = false) public Map<Long, byte[]> getThumbnailByLongestSideSet(Integer size, Set<Long> pixelsIds) { // Set defaults and sanity check thumbnail sizes Dimension checkedDimensions = sanityCheckThumbnailSizes(size, size); size = (int) checkedDimensions.getWidth(); // Prepare our thumbnail context newContext(); ctx.loadAndPrepareRenderingSettings(pixelsIds); ctx.createAndPrepareMissingRenderingSettings(pixelsIds); ctx.loadAndPrepareMetadata(pixelsIds, size); return retrieveThumbnailSet(pixelsIds); } /** * Performs the logic of retrieving a set of thumbnails. * @param pixelsIds The Pixels IDs to retrieve thumbnails for. * @return Map of Pixels ID vs. thumbnail bytes. */ private Map<Long, byte[]> retrieveThumbnailSet(Set<Long> pixelsIds) { // Our return value HashMap Map<Long, byte[]> toReturn = new HashMap<Long, byte[]>(); List<Thumbnail> toSave = new ArrayList<Thumbnail>(); for (Long pixelsId : pixelsIds) { // Ensure that the renderer has been made dirty otherwise the // same renderer will be used to return all thumbnails with dirty // metadata. (See #2075). resetMetadata(); try { if (!ctx.hasSettings(pixelsId)) { try { pixelDataService.getPixelBuffer(ctx.getPixels(pixelsId), false); continue; // No exception, not an in progress image } catch (ConcurrencyException e) { log.info("ConcurrencyException on " + "retrieveThumbnailSet.ctx.hasSettings"); inProgress = true; } } pixels = ctx.getPixels(pixelsId); pixelsId = pixels.getId(); settings = ctx.getSettings(pixelsId); thumbnailMetadata = ctx.getMetadata(pixelsId); try { byte[] thumbnail = retrieveThumbnail(); toReturn.put(pixelsId, thumbnail); if (dirtyMetadata) { toSave.add(thumbnailMetadata); } } finally { dirtyMetadata = false; } } catch (Throwable t) { log.warn("Retrieving thumbnail in set for " + "Pixels ID " + pixelsId + " failed.", t); toReturn.put(pixelsId, null); } } // We're doing the update or creation and save as a two step // process due to the possible unloaded Pixels. If we do not, // Pixels will be unloaded and we will hit // IllegalStateException's when checking update events. iUpdate.saveArray(toSave.toArray(new Thumbnail[toSave.size()])); // Ensure that we do not have "dirty" pixels or rendering settings left // around in the Hibernate session cache. iQuery.clear(); iUpdate.flush(); return toReturn; } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#getThumbnail(ome.model.core.Pixels, * ome.model.display.RenderingDef, java.lang.Integer, * java.lang.Integer) */ @RolesAllowed("user") @Transactional(readOnly = false) public byte[] getThumbnail(Integer sizeX, Integer sizeY) { errorIfNullPixelsAndRenderingDef(); Dimension dimensions = sanityCheckThumbnailSizes(sizeX, sizeY); // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); // Reloading thumbnail metadata because we don't know what may have // happened in the database since our last method call. Set<Long> pixelsIds = new HashSet<Long>(); pixelsIds.add(pixelsId); ctx.loadAndPrepareMetadata(pixelsIds, dimensions); thumbnailMetadata = ctx.getMetadata(pixelsId); return retrieveThumbnailAndUpdateMetadata(); } /** * Creates the thumbnail or retrieves it from cache and updates the * thumbnail metadata. * @return Thumbnail bytes. */ private byte[] retrieveThumbnailAndUpdateMetadata() { byte[] thumbnail = retrieveThumbnail(); if (dirtyMetadata) { try { iUpdate.saveObject(thumbnailMetadata); } finally { dirtyMetadata = false; } } return thumbnail; } /** * Creates the thumbnail or retrieves it from cache. * @return Thumbnail bytes. */ private byte[] retrieveThumbnail() { if (inProgress) { return retrieveThumbnailDirect(thumbnailMetadata.getSizeX(), thumbnailMetadata.getSizeY(), 0, 0); } try { boolean cached = ctx.isThumbnailCached(pixels.getId()); if (cached) { if (log.isDebugEnabled()) { log.debug("Cache hit."); } } else { if (log.isDebugEnabled()) { log.debug("Cache miss, thumbnail missing or out of date."); } _createThumbnail(); } byte[] thumbnail = ioService.getThumbnail(thumbnailMetadata); return thumbnail; } catch (IOException e) { log.error("Could not obtain thumbnail", e); throw new ResourceError(e.getMessage()); } } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#getThumbnailByLongestSide(ome.model.core.Pixels, * ome.model.display.RenderingDef, java.lang.Integer) */ @RolesAllowed("user") @Transactional(readOnly = false) public byte[] getThumbnailByLongestSide(Integer size) { errorIfNullPixelsAndRenderingDef(); // Set defaults and sanity check thumbnail sizes Dimension dimensions = sanityCheckThumbnailSizes(size, size); size = (int) dimensions.getWidth(); // Ensure that we do not have "dirty" pixels or rendering settings left // around in the Hibernate session cache. iQuery.clear(); // Resetting thumbnail metadata because we don't know what may have // happened in the database since or if sizeX and sizeY have changed. Set<Long> pixelsIds = new HashSet<Long>(); pixelsIds.add(pixelsId); ctx.loadAndPrepareMetadata(pixelsIds, size); thumbnailMetadata = ctx.getMetadata(pixelsId); return retrieveThumbnailAndUpdateMetadata(); } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#getThumbnailDirect(ome.model.core.Pixels, * ome.model.display.RenderingDef, java.lang.Integer, * java.lang.Integer) */ @RolesAllowed("user") public byte[] getThumbnailDirect(Integer sizeX, Integer sizeY) { // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); return retrieveThumbnailDirect(sizeX, sizeY, null, null); } /** * Retrieves a thumbnail directly, not inspecting or interacting with the * thumbnail cache. * @param sizeX Width of the thumbnail. * @param sizeY Height of the thumbnail. * @param theZ Optical section to retrieve a thumbnail for. * @param theT Timepoint to retrieve a thumbnail for. * @return */ private byte[] retrieveThumbnailDirect(Integer sizeX, Integer sizeY, Integer theZ, Integer theT) { errorIfNullPixelsAndRenderingDef(); // Set defaults and sanity check thumbnail sizes Dimension dimensions = sanityCheckThumbnailSizes(sizeX, sizeY); thumbnailMetadata = ctx.createThumbnailMetadata(pixels, dimensions); BufferedImage image = createScaledImage(theZ, theT); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); try { if (inProgress) { compressInProgressImageToStream(thumbnailMetadata, byteStream); } else { compressionService.compressToStream(image, byteStream); } byte[] thumbnail = byteStream.toByteArray(); return thumbnail; } catch (IOException e) { log.error("Could not obtain thumbnail direct.", e); throw new ResourceError(e.getMessage()); } finally { try { byteStream.close(); } catch (IOException e) { log.error("Could not close byte stream.", e); throw new ResourceError(e.getMessage()); } } } /* (non-Javadoc) * @see ome.api.ThumbnailStore#getThumbnailForSectionDirect(int, int, java.lang.Integer, java.lang.Integer) */ @RolesAllowed("user") public byte[] getThumbnailForSectionDirect(int theZ, int theT, Integer sizeX, Integer sizeY) { // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); return retrieveThumbnailDirect(sizeX, sizeY, theZ, theT); } /** Actually does the work specified by {@link getThumbnailByLongestSideDirect()}.*/ private byte[] _getThumbnailByLongestSideDirect(Integer size, Integer theZ, Integer theT) { // Sanity check thumbnail sizes Dimension dimensions = sanityCheckThumbnailSizes(size, size); dimensions = ctx.calculateXYWidths(pixels, (int) dimensions.getWidth()); return retrieveThumbnailDirect((int) dimensions.getWidth(), (int) dimensions.getHeight(), theZ, theT); } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#getThumbnailByLongestSideDirect(ome.model.core.Pixels, * ome.model.display.RenderingDef, java.lang.Integer) */ @RolesAllowed("user") public byte[] getThumbnailByLongestSideDirect(Integer size) { errorIfNullPixelsAndRenderingDef(); // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); return _getThumbnailByLongestSideDirect(size, null, null); } /* (non-Javadoc) * @see ome.api.ThumbnailStore#getThumbnailForSectionByLongestSideDirect(int, int, java.lang.Integer) */ @RolesAllowed("user") public byte[] getThumbnailForSectionByLongestSideDirect(int theZ, int theT, Integer size) { errorIfNullPixelsAndRenderingDef(); // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); return _getThumbnailByLongestSideDirect(size, theZ, theT); } /* * (non-Javadoc) * * @see ome.api.ThumbnailStore#thumbnailExists(ome.model.core.Pixels, * java.lang.Integer, java.lang.Integer) */ @RolesAllowed("user") public boolean thumbnailExists(Integer sizeX, Integer sizeY) { // Set defaults and sanity check thumbnail sizes errorIfNullPixelsAndRenderingDef(); if (inProgress) { return false; } Dimension dimensions = sanityCheckThumbnailSizes(sizeX, sizeY); Set<Long> pixelsIds = new HashSet<Long>(); pixelsIds.add(pixelsId); ctx.loadAndPrepareMetadata(pixelsIds, dimensions); // Ensure that we do not have "dirty" pixels or rendering settings // left around in the Hibernate session cache. iQuery.clear(); return ctx.isThumbnailCached(pixelsId); } @RolesAllowed("user") @Transactional(readOnly = false) public void resetDefaults() { if (settings == null && ctx.isExtendedGraphCritical(Collections.singleton(pixelsId))) { throw new ApiUsageException( "Unable to reset rendering settings in a read-only group " + "for Pixels set id:" + pixelsId); } _resetDefaults(); iUpdate.flush(); } /** Actually does the work specified by {@link resetDefaults()}.*/ private void _resetDefaults() { // Ensure that setPixelsId() has been called first. errorIfNullPixels(); // Ensure that we haven't just been called before setPixelsId() and that // the rendering settings are null. Parameters params = new Parameters(); params.addId(pixels.getId()); params.addLong("o_id", getCurrentUserId()); if (settings != null || iQuery.findByQuery( "from RenderingDef as r where r.pixels.id = :id and " + "r.details.owner.id = :o_id", params) != null) { throw new ApiUsageException("The thumbnail service only resets **empty** rendering " + "settings. Resetting of existing settings should either " + "be performed using the RenderingEngine or " + "IRenderingSettings."); } RenderingDef def = settingsService.createNewRenderingDef(pixels); try { settingsService.resetDefaults(def, pixels); } catch (ConcurrencyException mpe) { inProgress = true; log.info("ConcurrencyException on settingsSerice.resetDefaults"); } } public boolean isDiskSpaceChecking() { return diskSpaceChecking; } public void setDiskSpaceChecking(boolean diskSpaceChecking) { this.diskSpaceChecking = diskSpaceChecking; } }