/* * Copyright 2008 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.services.delete; import java.util.HashSet; import java.util.List; import java.util.Set; import ome.annotations.RolesAllowed; import ome.api.IDelete; import ome.api.ServiceInterface; import ome.api.local.LocalAdmin; import ome.conditions.ApiUsageException; import ome.conditions.SecurityViolation; import ome.conditions.ValidationException; import ome.logic.AbstractLevel2Service; import ome.model.IObject; import ome.model.annotations.ImageAnnotationLink; import ome.model.containers.DatasetImageLink; import ome.model.core.Channel; import ome.model.core.Image; import ome.model.core.LogicalChannel; import ome.model.core.Pixels; import ome.model.display.ChannelBinding; import ome.model.display.RenderingDef; import ome.model.internal.Details; import ome.model.screen.Plate; import ome.parameters.Parameters; import ome.security.AdminAction; import ome.security.SecuritySystem; import ome.system.EventContext; import ome.tools.hibernate.SessionFactory; import ome.util.CBlock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hibernate.Query; import org.hibernate.Session; import org.springframework.transaction.annotation.Transactional; /** * Strict implementation of the {@link IDelete} service interface which will use * the {@link SecuritySystem} via * {@link ome.security.SecuritySystem#runAsAdmin(AdminAction)} to forcibly * delete instances. * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 * @see IDelete */ @Transactional public class DeleteBean extends AbstractLevel2Service implements IDelete { public final static Logger log = LoggerFactory.getLogger(DeleteBean.class); /** * Loads an {@link Image} graph including: Pixels, Channel, LogicalChannel, * StatsInfo, PlaneInfo, Thumbnails, file maps, OriginalFiles, and Settings */ public final static String IMAGE_QUERY = "select i from Image as i " + "left outer join fetch i.pixels as p " + "left outer join fetch p.channels as c " + "left outer join fetch c.logicalChannel as lc " + "left outer join fetch lc.channels as c2 " + "left outer join fetch c.statsInfo as sinfo " + "left outer join fetch p.planeInfo as pinfo " + "left outer join fetch p.thumbnails as thumb " + "left outer join fetch p.pixelsFileMaps as map " + "left outer join fetch map.parent as ofile " + "left outer join fetch p.settings as setting " + "where i.id = :id"; public final static String SETTINGSID_QUERY = "select r.id, q.id from RenderingDef r " + "join r.quantization q " + "join r.pixels pix " + "join pix.image img where img.id = :id"; public final static String CHANNELID_QUERY = "select ch.id, si.id, lc.id " + "from Channel ch " + "join ch.statsInfo si " + "join ch.logicalChannel lc " + "join ch.pixels.image img where img.id = :id"; public final static String PLATEIMAGES_QUERY = "select i.id from Image i " + "join i.wellSamples ws join ws.well w " + "join w.plate p where p.id = :id"; protected final LocalAdmin admin; protected final SessionFactory sf; public final Class<? extends ServiceInterface> getServiceInterface() { return IDelete.class; } public DeleteBean(LocalAdmin admin, SessionFactory sf) { this.admin = admin; this.sf = sf; } // ~ Service Methods // ========================================================================= @RolesAllowed("user") public List<IObject> checkImageDelete(final long id, final boolean force) { final QueryConstraints constraints = new QueryConstraints(admin, iQuery, id, force); sec.runAsAdmin(constraints); return constraints.getResults(); } /** * This uses {@link #IMAGE_QUERY} to load all the subordinate metadata of the * {@link Image} which will be deleted. */ @RolesAllowed("user") public List<IObject> previewImageDelete(long id, boolean force) { final UnloadedCollector delete = new UnloadedCollector(iQuery, admin, false); Image[] holder = new Image[1]; getImageAndCount(holder, id, delete); return delete.list; } @RolesAllowed("user") public void deleteImage(final long id, final boolean force) throws SecurityViolation, ValidationException { final List<IObject> constraints = checkImageDelete(id, force); if (constraints.size() > 0) { throw new ApiUsageException( "Image has following constraints and cannot be deleted:" + constraints + "\nIt is possible to check for a " + "non-empty constraints list via checkImageDelete."); } final Image i = iQuery.get(Image.class, id); throwSecurityViolationIfNotAllowed(i); final Session session = sf.getSession(); session.clear(); sec.runAsAdmin(new AdminAction() { public void runAsAdmin() { clearRois(session, i); } }); /* Previously, the IMAGE_QUERY query was used to load all the objects attached to an Image for deletion. This, unfortunately, led to memory issues (ticket:1708). Now, instead, we are deleting the objects in the same order, but without loading them. */ execute(session, id, "update Pixels set relatedTo = null where id in" + "(select p.id from Pixels p where p.relatedTo.image.id = :id)"); execute(session, id, "delete PixelsOriginalFileMap where id in" + "(select m.id from PixelsOriginalFileMap m where m.child.image.id = :id)"); execute(session, id, "delete PlaneInfo where id in " + "(select pi.id from PlaneInfo pi where pi.pixels.image.id = :id)"); deleteSettings(id); deleteChannels(id); execute(session, id, "delete Thumbnail where id in " + "(select tb.id from Thumbnail tb where tb.pixels.image.id = :id)"); execute(session, id, "delete Pixels where id in " + "(select pix.id from Pixels pix where pix.image.id = :id)"); execute(session, id, "delete ImageAnnotationLink where id in " + "(select link.id from ImageAnnotationLink link where link.parent.id = :id)"); execute(session, id, "delete DatasetImageLink where id in " + "(select link.id from DatasetImageLink link where link.child.id = :id)"); execute(session, id, "delete Image img where img.id = :id"); session.clear(); // ticket:1708 } private int execute(final Session session, final long id, String str) { Query q; q = session.createQuery(str); q.setParameter("id", id); return q.executeUpdate(); } @RolesAllowed("user") public void deleteImages(java.util.Set<Long> ids, boolean force) throws SecurityViolation, ValidationException, ApiUsageException { if (ids == null || ids.size() == 0) { return; // EARLY EXIT! } for (Long id : ids) { try { deleteImage(id, force); } catch (SecurityViolation sv) { throw new SecurityViolation("Error while deleting image " + id + "\n" + sv.getMessage()); } catch (ValidationException ve) { throw new ValidationException("Error while deleting image " + id + "\n" + ve.getMessage()); } catch (ApiUsageException aue) { throw new ApiUsageException("Error while deleting image " + id + "\n" + aue.getMessage()); } } }; @RolesAllowed("user") public void deleteImagesByDataset(long datasetId, boolean force) throws SecurityViolation, ValidationException, ApiUsageException { List<Object[]> links = iQuery.projection( "select link.id, c.id from DatasetImageLink link " + "join link.parent p " + "join link.child c " + "where p.id = :id", new Parameters() .addId(datasetId)); Set<Long> ids = new HashSet<Long>(); for (Object[] link_child : links) { ids.add((Long)link_child[1]); iUpdate.deleteObject(new DatasetImageLink((Long)link_child[0], false)); } deleteImages(ids, force); }; @RolesAllowed("user") public void deleteSettings(final long imageId) { Image i = iQuery.get(Image.class, imageId); throwSecurityViolationIfNotAllowed(i); final Session session = sf.getSession(); sec.runAsAdmin(new AdminAction() { public void runAsAdmin() { List<Object[]> rdefs = iQuery.projection( SETTINGSID_QUERY, new Parameters().addId(imageId)); for (Object[] rv: rdefs) { Long rid = (Long) rv[0]; Long qid = (Long) rv[1]; Query q = session.createQuery("delete ChannelBinding cb where cb.renderingDef.id = :rid"); q.setParameter("rid", rid); q.executeUpdate(); q = session.createQuery("delete RenderingDef r where r.id = :rid"); q.setParameter("rid", rid); q.executeUpdate(); q = session.createQuery("delete QuantumDef q where q.id = :qid"); q.setParameter("qid", qid); q.executeUpdate(); } } }); } @RolesAllowed("user") public void deleteChannels(final long imageId) { Image i = iQuery.get(Image.class, imageId); throwSecurityViolationIfNotAllowed(i); final Session session = sf.getSession(); sec.runAsAdmin(new AdminAction() { public void runAsAdmin() { List<Object[]> channels = iQuery.projection( CHANNELID_QUERY, new Parameters().addId(imageId)); for (Object[] rv: channels) { Long chid = (Long) rv[0]; Long siid = (Long) rv[1]; Long lcid = (Long) rv[2]; execute(session, chid, "delete Channel ch where ch.id = :id"); execute(session, siid, "delete StatsInfo si where si.id = :id"); List<Object[]> remainingChannels = iQuery.projection( "select ch.id from LogicalChannel lc join lc.channels ch " + "where lc.id = :id", new Parameters().addId(lcid)); if (remainingChannels.size() == 0) { execute(session, lcid, "delete LogicalChannel lc where lc.id = :id"); } } } }); } @RolesAllowed("user") public void deletePlate(final long plateId) { Plate p = iQuery.get(Plate.class, plateId); throwSecurityViolationIfNotAllowed(p); sec.runAsAdmin(new AdminAction() { public void runAsAdmin() { final List<Object[]> imagesOnPlate = iQuery.projection( PLATEIMAGES_QUERY, new Parameters().addId(plateId)); final Session session = sf.getSession(); final StringBuilder sb = new StringBuilder(); sb.append("Delete for plate "); sb.append(plateId); sb.append(" : "); Query q; // reused. int count; // reused if (imagesOnPlate.size() > 0) { Set<Long> imageIdsForPlate = new HashSet<Long>(); for (Object[] objs: imagesOnPlate) { imageIdsForPlate.add((Long)objs[0]); } sb.append(imageIdsForPlate.size()); sb.append(" Image(s); "); // Samples q = session.createQuery("delete WellSample " + "where image.id in (:ids)"); q.setParameterList("ids", imageIdsForPlate); count = q.executeUpdate(); sb.append(count); sb.append(" WellSample(s); "); // Images deleteImages(imageIdsForPlate, true); } // Well q = session .createQuery("delete WellAnnotationLink where parent.id in " + "(select id from Well where plate.id = :id)"); q.setParameter("id", plateId); count = q.executeUpdate(); sb.append(count); sb.append(" WellAnnotationLink(s);"); q = session.createQuery("delete Well where plate.id = :id"); q.setParameter("id", plateId); count = q.executeUpdate(); sb.append(count); sb.append(" Well(s);"); // Plate annotations q = session .createQuery("delete PlateAnnotationLink where parent.id = :id"); q.setParameter("id", plateId); count = q.executeUpdate(); sb.append(count); sb.append(" PlateAnnotationLink(s);"); // Screen links q = session .createQuery("delete ScreenPlateLink where child.id = :id"); q.setParameter("id", plateId); count = q.executeUpdate(); sb.append(count); sb.append(" ScreenPlateLink(s);"); // Finally, the plate. q = session.createQuery("delete Plate where id = :id"); q.setParameter("id", plateId); q.executeUpdate(); iUpdate.flush(); log.info(sb.toString()); } }); } // Implementation // ========================================================================= /** * Uses the locally defined query to load an {@link Image} and calls * {@link #collect(UnloadedCollector, Image)} in order to define a list of * what will be deleted. * * This method fulfills the {@link #previewImageDelete(long, boolean)} * contract and as such is used by {@link #deleteImage(long, boolean)} in * order to fulfill its contract. */ protected void getImageAndCount(final Image[] images, final long id, final UnloadedCollector delete) { sec.runAsAdmin(new AdminAction() { public void runAsAdmin() { images[0] = iQuery.findByQuery(IMAGE_QUERY, new Parameters() .addId(id)); if (images[0] == null) { throw new ApiUsageException("Cannot find image: " + id); } collect(delete, images[0]); } }); } /** * Walks the {@link Image} graph collecting unloaded instances of all * entities for later delete. */ protected void collect(final UnloadedCollector delete, final Image i) { i.collectPixels(new CBlock<Pixels>() { public Pixels call(IObject object) { if (object == null) { return null; // EARLY EXIT. Happening due to image_index=1 } Pixels p = (Pixels) object; p.eachLinkedOriginalFile(delete); p.collectPlaneInfo(delete); for (RenderingDef rdef : p .collectSettings((CBlock<RenderingDef>) null)) { for (ChannelBinding binding : rdef .unmodifiableWaveRendering()) { delete.call(binding); } delete.call(rdef); delete.call(rdef.getQuantization()); } p.collectThumbnails(delete); // Why do we set channel to null here and not waveRendering // above? List<Channel> channels = p .collectChannels((CBlock<Channel>) null); for (int i = 0; i < channels.size(); i++) { Channel channel = channels.set(i, null); delete.call(channel); delete.call(channel.getStatsInfo()); LogicalChannel lc = channel.getLogicalChannel(); if (lc.sizeOfChannels() < 2) { delete.call(lc); } // delete.call(lc.getLightSource()); // // TODO lightsource // delete.call(lc.getAuxLightSource()); // // TODO lightsource // delete.call(lc.getOtf()); // delete.call(lc.getDetectorSettings()); // DetectorSettings ds = lc.getDetectorSettings(); // delete.call(ds.getDetector()); } delete.call(p); return null; } }); for (DatasetImageLink link : i .collectDatasetLinks((CBlock<DatasetImageLink>) null)) { i.removeDatasetImageLink(link, true); delete.call(link); } for (ImageAnnotationLink link : i .collectAnnotationLinks((CBlock<ImageAnnotationLink>) null)) { i.removeImageAnnotationLink(link, true); delete.call(link); } delete.call(i); } private void throwSecurityViolationIfNotAllowed(final IObject i) { final String type = i.getClass().getName(); final Details d = i.getDetails(); final long user = d.getOwner().getId(); final long group = d.getGroup().getId(); final EventContext ec = getSecuritySystem().getEventContext(); final boolean root = ec.isCurrentUserAdmin(); final List<Long> leaderof = ec.getLeaderOfGroupsList(); final boolean pi = leaderof.contains(group); final boolean own = ec.getCurrentUserId().equals(user); if (!own && !root && !pi) { if (log.isWarnEnabled()) { log.warn(String.format("User %d attempted to delete " + type + " %d belonging to User %d", ec.getCurrentUserId(), i .getId(), user)); } throw new SecurityViolation(String.format( "User %s cannot delete %s %d ", ec.getCurrentUserName(), type, i.getId())); } } /** * Uses bulk update * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1654">ticket:1654</a> */ private void clearRois(Session session, Image i) { int shapeCount = execute(session, i.getId(), "delete from Shape where roi.id in " + "(select id from Roi roi where roi.image.id = :id)"); int roiAnnCount = execute(session, i.getId(), "delete from RoiAnnotationLink where parent.id in " + "(select id from Roi roi where roi.image.id = :id)"); int roiCount = execute(session, i.getId(), "delete from Roi where image.id = :id"); if (shapeCount > 0 || roiAnnCount > 0 || roiCount > 0) { log.info(String.format("Roi delete for image %s :" + " %s rois, %s shapes, %s annotations", i.getId(), roiCount, shapeCount, roiAnnCount)); } } }