/*
* Copyright 2006-2013 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.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.ReadOnlyGroupSecurityViolation;
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.parameters.Parameters;
import ome.services.ThumbnailCtx.NoThumbnail;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
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;
/**
* Version integer which will be used when a thumbnail is saved that is
* currently having its pyramid generated, i.e. isInProgress.
*/
private static final Integer PROGRESS_VERSION = -1;
/** The logger for this class. */
private transient static Logger log = LoggerFactory.getLogger(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 #ctx} 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() {}
/**
* overridden 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)
{
newContext();
}
Set<Long> pixelsIds = new HashSet<Long>();
pixelsIds.add(id);
ctx.loadAndPrepareRenderingSettings(pixelsIds);
pixels = ctx.getPixels(id);
pixelsId = pixels.getId();
settings = ctx.getSettings(id);
return (ctx.hasSettings(id));
}
/*
* (non-Javadoc)
*
* @see ome.api.ThumbnailStore#isInProgress()
*/
@RolesAllowed("user")
public boolean isInProgress()
{
return inProgress;
}
/**
* 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 data service Bean injector.
*
* @param pixelDataService
* a <code>PixelsService</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 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
* 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, sec.getEffectiveUID());
}
/**
* 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);
try {
thumbnailMetadata = ctx.getMetadata(pixels.getId());
} catch (NoThumbnail e) {
// See #10618
// Since the creation of the thumbnail was explicitly requested,
// we'll throw an exception instead.
throw new ValidationException(e.getMessage());
}
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 Slf4JStopWatch("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.
//Check first if the thumbnail is the one of the settings owner
Long ownerId = thumbnailMetadata.getDetails().getOwner().getId();
Long rndOwnerId = settings.getDetails().getOwner().getId();
if (rndOwnerId.equals(ownerId)) {
Pixels unloadedPixels = new Pixels(pixels.getId(), false);
thumbnailMetadata.setPixels(unloadedPixels);
_setMetadataVersion(thumbnailMetadata, inProgress);
dirtyMetadata = true;
} else {
//new one for owner of the settings.
Dimension d = new Dimension(thumbnailMetadata.getSizeX(),
thumbnailMetadata.getSizeY());
thumbnailMetadata = ctx.createThumbnailMetadata(pixels, d);
_setMetadataVersion(thumbnailMetadata, inProgress);
thumbnailMetadata = iUpdate.saveAndReturnObject(thumbnailMetadata);
dirtyMetadata = false;
}
}
// 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());
}
}
private static void _setMetadataVersion(Thumbnail tb, boolean inProgress) {
Integer version = tb.getVersion();
if (version == null) {
version = inProgress ? PROGRESS_VERSION : 0;
} else {
version++;
}
tb.setVersion(version);
}
/*
* (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);
Map<Long, byte[]> values = retrieveThumbnailSet(pixelsIds);
iQuery.clear();
return values;
}
@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);
Map<Long, byte[]> values = retrieveThumbnailSet(pixelsIds);
iQuery.clear();
return values;
}
/**
* 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.debug("ConcurrencyException on " +
"retrieveThumbnailSet.ctx.hasSettings: " +
"pyramid in progress");
inProgress = true;
}
}
pixels = ctx.getPixels(pixelsId);
pixelsId = pixels.getId();
settings = ctx.getSettings(pixelsId);
thumbnailMetadata = ctx.getMetadata(pixelsId);
if (!PROGRESS_VERSION.equals(thumbnailMetadata.getVersion())) {
thumbnailMetadata.setVersion(PROGRESS_VERSION);
dirtyMetadata = true;
}
try
{
// At this point, we're sure that we have a thumbnail obj
// that we want to use, but retrieveThumbnail likes to
// re-generate. For the moment, we're saving and restoring
// that value to prevent creating a new one.
byte[] thumbnail = retrieveThumbnail(false);
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);
// 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);
byte[] value = null;
try {
ctx.loadAndPrepareMetadata(pixelsIds, dimensions);
thumbnailMetadata = ctx.getMetadata(pixelsId);
value = retrieveThumbnailAndUpdateMetadata(false);
} catch (Throwable t) {
value = handleNoThumbnail(t, dimensions);
}
iQuery.clear();//see #11072
return value;
}
/**
* Creates the thumbnail or retrieves it from cache and updates the
* thumbnail metadata.
* @return Thumbnail bytes.
*/
private byte[] retrieveThumbnailAndUpdateMetadata(boolean rewriteMetadata)
{
byte[] thumbnail = retrieveThumbnail(rewriteMetadata);
if (inProgress && !PROGRESS_VERSION.equals(thumbnailMetadata)) {
thumbnailMetadata.setVersion(PROGRESS_VERSION);
dirtyMetadata = true;
}
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(boolean rewriteMetadata)
{
if (inProgress)
{
return retrieveThumbnailDirect(
thumbnailMetadata.getSizeX(),
thumbnailMetadata.getSizeY(),
0, 0, rewriteMetadata);
}
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();
// 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);
byte[] value = null;
try {
ctx.loadAndPrepareMetadata(pixelsIds, size);
thumbnailMetadata = ctx.getMetadata(pixelsId);
value = retrieveThumbnailAndUpdateMetadata(false);
} catch (Throwable t) {
value = handleNoThumbnail(t, dimensions);
}
iQuery.clear();//see #11072
return value;
}
/*
* (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)
{
// Leaving rewriteMetadata here true since it's unclear of what the
// expected state of the bean should be.
byte[] value = retrieveThumbnailDirect(sizeX, sizeY, null, null, true);
// Ensure that we do not have "dirty" pixels or rendering settings
// left around in the Hibernate session cache.
iQuery.clear();
return value;
}
/**
* 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, boolean rewriteMetadata)
{
errorIfNullPixelsAndRenderingDef();
// Set defaults and sanity check thumbnail sizes
Dimension dimensions = sanityCheckThumbnailSizes(sizeX, sizeY);
Thumbnail local = ctx.createThumbnailMetadata(pixels, dimensions);
if (rewriteMetadata) {
thumbnailMetadata = local;
}
BufferedImage image = createScaledImage(theZ, theT);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
try {
if (inProgress) {
compressInProgressImageToStream(local, 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)
{
// As getThumbnailDirect
byte[] value = retrieveThumbnailDirect(sizeX, sizeY, theZ, theT, true);
// Ensure that we do not have "dirty" pixels or rendering settings
// left around in the Hibernate session cache.
iQuery.clear();
return value;
}
/** Actually does the work specified by {@link getThumbnailByLongestSideDirect()}.*/
private byte[] _getThumbnailByLongestSideDirect(Integer size, Integer theZ,
Integer theT, boolean rewriteMetadata)
{
// Sanity check thumbnail sizes
Dimension dimensions = sanityCheckThumbnailSizes(size, size);
dimensions = ctx.calculateXYWidths(pixels, (int) dimensions.getWidth());
byte[] value = retrieveThumbnailDirect((int) dimensions.getWidth(),
(int) dimensions.getHeight(), theZ, theT, rewriteMetadata);
iQuery.clear();
return value;
}
/*
* (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();
// As getThumbnailDirect
byte[] value = _getThumbnailByLongestSideDirect(size, null, null, true);
// Ensure that we do not have "dirty" pixels or rendering settings
// left around in the Hibernate session cache.
iQuery.clear();//see #11072
return value;
}
/* (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();
// As getThumbnailDirect
byte[] value = _getThumbnailByLongestSideDirect(size, theZ, theT, true);
// Ensure that we do not have "dirty" pixels or rendering settings
// left around in the Hibernate session cache.
iQuery.clear();
return value;
}
/*
* (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, false);
// 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", sec.getEffectiveUID());
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;
}
/**
* If a known exception is thrown, then fallback to using a direct
* thumbnail generation method. Otherwise, re-throw the exception,
* wrapping it as necessary.
*/
private byte[] handleNoThumbnail(Throwable t, Dimension dimensions) {
if (t instanceof NoThumbnail ||
t instanceof ReadOnlyGroupSecurityViolation) {
log.debug("Calling retrieveThumbnailDirect on missing thumbnail");
// As getThumbnailDirect
return retrieveThumbnailDirect((int) dimensions.getWidth(),
(int) dimensions.getHeight(), null, null, true);
} else if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
// This is unexpected. The only checked exception that
// should be throwable by the invoking methods should be
// NoThumbnail.
InternalException ie = new InternalException("No thumbnail available!");
ie.initCause(t);
throw ie;
}
}
}