/*
* Copyright 2010 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package integration.delete;
import static omero.rtypes.rbool;
import static omero.rtypes.rstring;
import integration.AbstractServerTest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import ome.parameters.Parameters;
import omero.RLong;
import omero.RType;
import omero.ServerError;
import omero.rtypes;
import omero.api.IAdminPrx;
import omero.api.IQueryPrx;
import omero.api.IUpdatePrx;
import omero.api.ServiceFactoryPrx;
import omero.cmd.Delete2;
import omero.gateway.util.Requests;
import omero.model.Annotation;
import omero.model.CommentAnnotationI;
import omero.model.Dataset;
import omero.model.DatasetI;
import omero.model.DatasetImageLink;
import omero.model.DatasetImageLinkI;
import omero.model.Experimenter;
import omero.model.ExperimenterGroup;
import omero.model.ExperimenterGroupI;
import omero.model.Image;
import omero.model.ImageAnnotationLink;
import omero.model.ImageAnnotationLinkI;
import omero.model.ImageI;
import omero.model.Instrument;
import omero.model.Pixels;
import omero.model.Plate;
import omero.model.PlateAcquisition;
import omero.model.PlateAcquisitionI;
import omero.model.PlateI;
import omero.model.Project;
import omero.model.ProjectI;
import omero.model.Roi;
import omero.model.Screen;
import omero.model.ScreenI;
import omero.model.Well;
import omero.model.WellI;
import omero.model.WellSample;
import omero.model.WellSampleI;
import omero.sys.ParametersI;
import org.apache.commons.collections.CollectionUtils;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
/**
* Tests for deleting hierarchies and the effects that that should have under
* double- and multiple-linking.
*
* @see ticket:3031
* @see ticket:2994
* @since 4.2.1
*/
@Test(groups = "ticket:2615")
public class HierarchyDeleteTest extends AbstractServerTest {
private final static omero.RString t3031 = rstring("#3031");
/**
* Test to delete a dataset containing an image also contained in another
* dataset. The second dataset and the image should not be deleted.
*
* @throws Exception
* Thrown if an error occurred.
*/
public void testDeletingDataset() throws Exception {
newUserAndGroup("rwrw--");
Dataset ds1 = new DatasetI();
ds1.setName(t3031);
Dataset ds2 = new DatasetI();
ds2.setName(t3031);
Image i1 = (Image) iUpdate.saveAndReturnObject(mmFactory.createImage());
i1.unload();
ds1.linkImage(i1);
ds1 = (Dataset) iUpdate.saveAndReturnObject(ds1);
ds2.linkImage(i1);
ds2 = (Dataset) iUpdate.saveAndReturnObject(ds2);
// http://trac.openmicroscopy.org.uk/ome/ticket/3031#comment:7
// This image is only singly linked and should be removed.
Image i2 = (Image) iUpdate.saveAndReturnObject(mmFactory.createImage());
i2.unload();
ds2.linkImage(i2);
ds2 = (Dataset) iUpdate.saveAndReturnObject(ds2);
final Delete2 dc = Requests.delete("Dataset", ds2.getId().getValue());
callback(true, client, dc);
assertDoesNotExist(ds2);
assertDoesNotExist(i2);
assertExists(ds1);
assertExists(i1);
}
/**
* Test to delete a dataset containing an image also contained in another
* dataset. The second dataset and the image with an annotation should not
* be deleted.
*
* @throws Exception
* Thrown if an error occurred.
*/
public void testDeletingDatasetDoesntRemoveImageAnnotation()
throws Exception {
newUserAndGroup("rwrw--");
Dataset ds1 = new DatasetI();
ds1.setName(t3031);
Dataset ds2 = new DatasetI();
ds2.setName(t3031);
Image i = (Image) iUpdate.saveAndReturnObject(mmFactory.createImage());
i.unload();
ds1.linkImage(i);
ds1 = (Dataset) iUpdate.saveAndReturnObject(ds1);
ds2.linkImage(i);
ds2 = (Dataset) iUpdate.saveAndReturnObject(ds2);
Annotation a = (Annotation) iUpdate
.saveAndReturnObject(new CommentAnnotationI());
ImageAnnotationLink link = new ImageAnnotationLinkI();
link.setChild((Annotation) a.proxy());
link.setParent((Image) i.proxy());
iUpdate.saveAndReturnObject(link);
final Delete2 dc = Requests.delete("Dataset", ds2.getId().getValue());
callback(true, client, dc);
assertDoesNotExist(ds2);
assertExists(ds1);
assertExists(i);
assertExists(a);
}
/**
* Test to delete a dataset containing an image also contained in another
* dataset. The second dataset and the image with ROI should not be deleted.
*
* @throws Exception
* Thrown if an error occurred.
*/
public void testDeletingDatasetDoesntRemoveImageRoi() throws Exception {
newUserAndGroup("rwrw--");
Dataset ds1 = new DatasetI();
ds1.setName(t3031);
Dataset ds2 = new DatasetI();
ds2.setName(t3031);
Image i = (Image) iUpdate.saveAndReturnObject(mmFactory
.createImageWithRoi());
Roi roi = i.copyRois().get(0);
i.unload();
ds1.linkImage(i);
ds1 = (Dataset) iUpdate.saveAndReturnObject(ds1);
ds2.linkImage(i);
ds2 = (Dataset) iUpdate.saveAndReturnObject(ds2);
final Delete2 dc = Requests.delete("Dataset", ds2.getId().getValue());
callback(true, client, dc);
assertDoesNotExist(ds2);
assertExists(ds1);
assertExists(i);
assertExists(roi);
}
/**
* Test to delete a project containing a dataset also contained in another
* project. The second project and the dataset should not be deleted.
*
* @throws Exception
* Thrown if an error occurred.
*/
@Test(groups = "ticket:3031")
public void testDeletingProject() throws Exception {
newUserAndGroup("rwrw--");
Project p1 = new ProjectI();
p1.setName(t3031);
Project p2 = new ProjectI();
p2.setName(t3031);
Dataset d = new DatasetI();
d.setName(t3031);
d = (Dataset) iUpdate.saveAndReturnObject(d);
d.unload();
p1.linkDataset(d);
p1 = (Project) iUpdate.saveAndReturnObject(p1);
p2.linkDataset(d);
p2 = (Project) iUpdate.saveAndReturnObject(p2);
final Delete2 dc = Requests.delete("Project", p2.getId().getValue());
callback(true, client, dc);
assertDoesNotExist(p2);
assertExists(p1);
assertExists(d);
}
/**
* Test to delete a screen containing a plate also contained in another
* screen. The second screen and the plate should not be deleted.
*
* @throws Exception
* Thrown if an error occurred.
*/
@Test(groups = "ticket:3031")
public void testDeletingScreen() throws Exception {
newUserAndGroup("rwrw--");
Screen s1 = new ScreenI();
s1.setName(t3031);
Screen s2 = new ScreenI();
s2.setName(t3031);
Plate p = mmFactory.createPlate(1, 1, 1, 1, false);
p = (Plate) iUpdate.saveAndReturnObject(p);
p.unload();
s1.linkPlate(p);
s1 = (Screen) iUpdate.saveAndReturnObject(s1);
s2.linkPlate(p);
s2 = (Screen) iUpdate.saveAndReturnObject(s2);
final Delete2 dc = Requests.delete("Screen", s2.getId().getValue());
callback(true, client, dc);
assertDoesNotExist(s2);
assertExists(s1);
assertExists(p);
}
/**
* Test that the correct wells, samples and images are deleted when runs of a plate are deleted.
* @throws Exception unexpected
*/
@Test(groups = "ticket:10564")
public void testDeletingMultipleRunsWithOrphanedSamples() throws Exception {
/* Create a new group. */
final IAdminPrx rootAdminSvc = root.getSession().getAdminService();
final String normalGroupName = UUID.randomUUID().toString();
ExperimenterGroup normalGroup = new ExperimenterGroupI();
normalGroup.setName(rstring(normalGroupName));
normalGroup.setLdap(rbool(false));
normalGroup = rootAdminSvc.getGroup(rootAdminSvc.createGroup(normalGroup));
/* Create a new user in that group. */
final String userName = UUID.randomUUID().toString();
final Experimenter experimenter = createExperimenterI(userName, "a", "user");
long eid = newUserInGroupWithPassword(experimenter, normalGroup, userName);
rootAdminSvc.setDefaultGroup(rootAdminSvc.getExperimenter(eid), normalGroup);
/* Create a session for the new user and obtain its query and update services. */
final omero.client client = newOmeroClient();
final ServiceFactoryPrx services = client.createSession(userName, userName);
final IQueryPrx iQuery = services.getQueryService();
final IUpdatePrx iUpdate = services.getUpdateService();
/* Set up the size of the test.
* The number of wells depends upon the number of runs because each well has a non-empty
* set of well samples, each of which may be tied to some run or to no run. Many well samples
* are created, to tie wells to every combination of runs and just to the plate. */
final int numberOfRuns = 3;
final int numberOfWells = (2 << numberOfRuns) - 1;
/* Create the plate. */
final Plate plate = new PlateI();
plate.setName(rtypes.rstring(UUID.randomUUID().toString()));
plate.setRows(rtypes.rint(1));
plate.setColumns(rtypes.rint(numberOfWells));
/* Create the wells of the plate. */
final List<Well> wells = new ArrayList<Well>(numberOfWells);
for (int columnNo = 1; columnNo <= numberOfWells; columnNo++) {
final Well well = new WellI();
well.setRow(rtypes.rint(1));
well.setColumn(rtypes.rint(columnNo));
plate.addWell(well);
wells.add(well);
}
/* Create the runs of the plate. */
final List<PlateAcquisition> runs = new ArrayList<PlateAcquisition>(numberOfRuns);
for (int runNo = 1; runNo <= numberOfRuns; runNo++) {
final PlateAcquisition run = new PlateAcquisitionI();
run.setName(rtypes.rstring(UUID.randomUUID().toString()));
plate.addPlateAcquisition(run);
runs.add(run);
}
/* Create well samples to make each well a different combination
* of being tied to runs and to only the plate. */
/* Note that this runNo is 0-indexed instead of 1-indexed:
* the last number represents just the plate rather than a specific run. */
for (int wellIndex = 0; wellIndex < numberOfWells; wellIndex++) {
for (int runNo = 0; runNo <= numberOfRuns; runNo++) {
if ((wellIndex & (1 << runNo)) == 0) {
final WellSample sample = new WellSampleI();
sample.setImage(this.mmFactory.createImage());
wells.get(wellIndex).addWellSample(sample);
if (runNo < numberOfRuns) {
runs.get(runNo).addWellSample(sample);
}
}
}
}
/* Persist the assembled plate. */
final long plateId = iUpdate.saveAndReturnObject(plate).getId().getValue();
/* Create indices of the resulting entity IDs for the plate and how they are linked. */
final Set<Long> runIds = new HashSet<Long>();
final Multimap<Long, Long> runsToSamples = HashMultimap.create();
final Multimap<Long, Long> wellsToRuns = HashMultimap.create();
final Map<Long, Long> samplesToWells = new HashMap<Long, Long>();
final Map<Long, Long> samplesToImages = new HashMap<Long, Long>();
/* Index runs, wells and samples. */
for (final List<RType> mapping : iQuery.projection(
"SELECT sample.plateAcquisition.id, well.id, sample.id FROM Well well, WellSample sample " +
"WHERE well.id = sample.well.id AND well.plate.id = :" + Parameters.ID,
new ParametersI().addId(plateId))) {
final Long runId = mapping.get(0) == null ? null : ((RLong) mapping.get(0)).getValue();
final long wellId = ((RLong) mapping.get(1)).getValue();
final long sampleId = ((RLong) mapping.get(2)).getValue();
if (runId != null) {
runIds.add(runId);
}
runsToSamples.put(runId, sampleId);
wellsToRuns.put(wellId, runId);
samplesToWells.put(sampleId, wellId);
}
/* Index samples and images. */
for (final List<RType> mapping : iQuery.projection(
"SELECT sample.id, sample.image.id FROM WellSample sample " +
"WHERE sample.id IN (:" + Parameters.IDS + ")",
new ParametersI().addIds(samplesToWells.keySet()))) {
final long sampleId = ((RLong) mapping.get(0)).getValue();
final long imageId = ((RLong) mapping.get(1)).getValue();
samplesToImages.put(sampleId, imageId);
}
/* Ensure that the wells are indeed tied to every combination of the expected number of runs. */
final Set<Collection<Long>> runCombinations = new HashSet<Collection<Long>>(numberOfWells);
for (final Collection<Long> runCombination : wellsToRuns.asMap().values()) {
Assert.assertTrue(runCombinations.add(runCombination),
"expecting each sample to be in a unique combination of runs");
}
Assert.assertEquals(runCombinations.size(), numberOfWells,
"expecting each run combination to be represented in a well");
Assert.assertEquals(runIds.size(), numberOfRuns,
"expecting the plate to have the correct number of runs");
/* Before deleting, note the IDs of all the entities that once existed,
* so that their deletion (rather than just unlinking) may be verified. */
final ImmutableSet<Long> allRuns = ImmutableSet.copyOf(runIds);
final ImmutableSet<Long> allWells = ImmutableSet.copyOf(samplesToWells.values());
final ImmutableSet<Long> allSamples = ImmutableSet.copyOf(samplesToWells.keySet());
final ImmutableSet<Long> allImages = ImmutableSet.copyOf(samplesToImages.values());
/* Delete all the plate's runs one by one, ensuring that entities are deleted accordingly. */
while (true) {
/* Choose the next run to delete. */
final Iterator<Long> runIdsIterator = runIds.iterator();
if (!runIdsIterator.hasNext()) {
break;
}
final long runIdToDelete = runIdsIterator.next();
runIdsIterator.remove();
/* Follow links to determine which entities are expected to remain afterward. */
final Set<Long> expectedRuns = new HashSet<Long>();
final Set<Long> expectedWells = allWells;
final Set<Long> expectedSamples = new HashSet<Long>();
final Set<Long> expectedImages = new HashSet<Long>();
expectedRuns.addAll(runIds);
expectedSamples.addAll(runsToSamples.get(null)); /* well samples not in runs */
for (final Long runId : expectedRuns) {
expectedSamples.addAll(runsToSamples.get(runId));
}
for (final Long sampleId : expectedSamples) {
/* expectedWells.add(samplesToWells.get(sampleId));
* if deleting a well's samples ought to delete the well
*/
expectedImages.add(samplesToImages.get(sampleId));
}
/* Delete the run. */
final Delete2 dc = Requests.delete("PlateAcquisition", runIdToDelete);
callback(true, client, dc);
/* Verify that exactly the expected entities remain. */
checkIds(iQuery, "PlateAcquisition", allRuns, expectedRuns);
checkIds(iQuery, "Well", allWells, expectedWells);
checkIds(iQuery, "WellSample", allSamples, expectedSamples);
checkIds(iQuery, "Image", allImages, expectedImages);
}
/* Delete the plate. */
final Delete2 dc = Requests.delete("Plate", plateId);
callback(true, client, dc);
/* Verify that no entities remain. */
checkIds(iQuery, "PlateAcquisition", allRuns, Collections.<Long>emptySet());
checkIds(iQuery, "Well", allWells, Collections.<Long>emptySet());
checkIds(iQuery, "WellSample", allSamples, Collections.<Long>emptySet());
checkIds(iQuery, "Image", allImages, Collections.<Long>emptySet());
/* Close the new user's session. */
client.closeSession();
}
/**
* Verify that the persisted entities are exactly as expected from among a superset.
* @param iQuery the query service to use
* @param entityClass the name of the entity class to query
* @param allIds the IDs of the entities that may exist
* @param expectedIds the IDs of the entities that should exist, a subset of <code>allIds</code>
* @throws ServerError if the query fails
*/
private void checkIds(IQueryPrx iQuery, String entityClass, Collection<Long> allIds, Collection<Long> expectedIds)
throws ServerError {
final Set<Long> actualIds = new HashSet<Long>(expectedIds.size());
for (final List<RType> wrappedId : iQuery.projection(
"SELECT id FROM " + entityClass + " WHERE id IN (:" + Parameters.IDS + ")",
new ParametersI().addIds(allIds))) {
actualIds.add(((RLong) wrappedId.get(0)).getValue());
}
Assert.assertTrue(CollectionUtils.isEqualCollection(actualIds, expectedIds),
"persisted entities not as expected: " + entityClass);
}
/**
* Test to delete a dataset containing an image whose projection is not in the dataset.
* The image should be deleted, but not its projection.
* @throws Exception unexpected
*/
@Test(groups = {"ticket:12856"})
public void testDeletingDatasetWithProjectedImage() throws Exception {
newUserAndGroup("rw----");
/* create a dataset */
Dataset dataset = new DatasetI();
dataset.setName(omero.rtypes.rstring("ticket #12856"));
dataset = (Dataset) iUpdate.saveAndReturnObject(dataset).proxy();
/* create an instrument */
Instrument instrument = mmFactory.createInstrument();
instrument = (Instrument) iUpdate.saveAndReturnObject(instrument).proxy();
/* the original and its projection are related and share an instrument */
Image original = mmFactory.createImage();
original.setInstrument(instrument);
original = (Image) iUpdate.saveAndReturnObject(original);
Image projection = mmFactory.createImage();
projection.setInstrument(instrument);
projection.getPrimaryPixels().setRelatedTo((Pixels) original.getPrimaryPixels().proxy());
projection = (Image) iUpdate.saveAndReturnObject(projection);
original = (Image) original.proxy();
projection = (Image) projection.proxy();
/* only the original is in the dataset */
final DatasetImageLink link = new DatasetImageLinkI();
link.setParent(dataset);
link.setChild(original);
iUpdate.saveAndReturnObject(link);
/* delete the dataset */
final Delete2 delete = Requests.delete("Dataset", dataset.getId().getValue());
callback(true, client, delete);
/* check what is left afterward */
assertDoesNotExist(dataset);
assertDoesNotExist(original);
assertExists(projection);
}
/* for dataset to plate test cases */
private enum Target { DATASET, IMAGES, PLATE };
/**
* Test deletion on a dataset, plate, or images, where the plate's images are in the dataset.
* @param target the target of the delete operation
* @throws Exception unexpected
*/
@Test(dataProvider = "dataset to plate test cases")
public void testDeletingDatasetToPlate(Target target) throws Exception {
newUserAndGroup("rw----");
/* create a plate */
Plate plate = mmFactory.createPlate(2, 2, 1, 1, false);
plate = (Plate) iUpdate.saveAndReturnObject(plate).proxy();
final long plateId = plate.getId().getValue();
/* find the plate's images */
final List<Long> imageIds = new ArrayList<Long>();
final String hql = "SELECT image.id FROM WellSample where well.id IN (SELECT id FROM Well WHERE plate.id = :id)";
final omero.sys.Parameters params = new ParametersI().addId(plateId);
for (final List<RType> result : iQuery.projection(hql, params)) {
final Long imageId = ((RLong) result.get(0)).getValue();
imageIds.add(imageId);
}
/* create the dataset and link the images to it */
final Dataset dataset = (Dataset) iUpdate.saveAndReturnObject(mmFactory.simpleDataset()).proxy();
final long datasetId = dataset.getId().getValue();
final List<Long> linkIds = new ArrayList<Long>(imageIds.size());
for (final long imageId : imageIds) {
DatasetImageLink link = new DatasetImageLinkI();
link.setParent(dataset);
link.setChild(new ImageI(imageId, false));
linkIds.add(iUpdate.saveAndReturnObject(link).getId().getValue());
}
/* perform the deletion */
final boolean isExpectSuccess = target != Target.IMAGES;
final Delete2 delete;
switch (target) {
case DATASET:
delete = Requests.delete("Dataset", datasetId);
break;
case IMAGES:
delete = Requests.delete("Image", imageIds);
break;
case PLATE:
delete = Requests.delete("Plate", plateId);
break;
default:
delete = null;
Assert.fail("unexpected target for delete");
}
doChange(client, factory, delete, isExpectSuccess);
if (isExpectSuccess) {
/* check that exactly the expected objects were deleted */
switch (target) {
case DATASET:
assertDoesNotExist("Dataset", datasetId);
assertAllExist("Image", imageIds);
assertNoneExist("DatasetImageLink", linkIds);
assertExists("Plate", plateId);
break;
case IMAGES:
Assert.fail("cannot delete images while used by well samples");
case PLATE:
assertExists("Dataset", datasetId);
assertAllExist("Image", imageIds);
assertAllExist("DatasetImageLink", linkIds);
assertDoesNotExist("Plate", plateId);
break;
}
}
/* delete the remaining "containers" */
switch (target) {
case DATASET:
delete.targetObjects = ImmutableMap.of("Plate", Collections.singletonList(plateId));
break;
case IMAGES:
delete.targetObjects = ImmutableMap.of(
"Dataset", Collections.singletonList(datasetId),
"Plate", Collections.singletonList(plateId));
break;
case PLATE:
delete.targetObjects = ImmutableMap.of("Dataset", Collections.singletonList(datasetId));
break;
}
doChange(client, factory, delete, true);
/* check that all the objects are now deleted */
assertDoesNotExist("Dataset", datasetId);
assertNoneExist("Image", imageIds);
assertNoneExist("DatasetImageLink", linkIds);
assertDoesNotExist("Plate", plateId);
}
/**
* @return a variety of test cases for dataset to plate
*/
@DataProvider(name = "dataset to plate test cases")
public Object[][] provideDatasetToPlateCases() {
int index = 0;
final int TARGET = index++;
final List<Object[]> testCases = new ArrayList<Object[]>();
for (final Target target : Target.values()) {
final Object[] testCase = new Object[index];
testCase[TARGET] = target;
// DEBUG: if (target == Target.DATASET)
testCases.add(testCase);
}
return testCases.toArray(new Object[testCases.size()][]);
}
}