/* * Copyright (C) 2015 University of Dundee & Open Microscopy Environment. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package omero.cmd.fs; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; 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 loci.formats.FormatException; import loci.formats.IFormatReader; import org.hibernate.Query; import org.hibernate.Session; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import ome.io.nio.PixelsService; import ome.model.core.Image; import ome.model.core.Pixels; import ome.system.Login; import ome.util.SqlAction; import omero.cmd.ERR; import omero.cmd.Helper; import omero.cmd.IRequest; import omero.cmd.Response; import omero.cmd.UsedFilesRequest; import omero.cmd.HandleI.Cancel; import omero.cmd.UsedFilesResponse; import omero.cmd.UsedFilesResponsePreFs; import omero.constants.annotation.file.ORIGINALMETADATA; import omero.constants.namespaces.NSCOMPANIONFILE; /** * Lists the IDs of the original files associated with an image. * @author m.t.b.carroll@dundee.ac.uk * @since 5.1.0 */ public class UsedFilesRequestI extends UsedFilesRequest implements IRequest { private static final long serialVersionUID = -1572148877023558009L; private static final ImmutableMap<String, String> ALL_GROUPS_CONTEXT = ImmutableMap.of(Login.OMERO_GROUP, "-1"); private final PixelsService pixelsService; private final Map<String, File> repositoryRoots = new HashMap<String, File>(); private Helper helper; private Session session; private Long filesetId; private Long pixelsId; private IFormatReader reader; /** * Construct a new used files request. * @param pixelsService the pixels service */ public UsedFilesRequestI(PixelsService pixelsService) { this.pixelsService = pixelsService; } /* FIT USED FILES INTO A SINGLE REQUEST STEP */ @Override public Map<String, String> getCallContext() { return new HashMap<String, String>(ALL_GROUPS_CONTEXT); } @Override public void init(Helper helper) { this.helper = helper; this.session = helper.getSession(); helper.setSteps(1); } @Override public Object step(int step) throws Cancel { helper.assertStep(step); try { switch (step) { case 0: return determineResponse(); default: final Exception e = new IllegalArgumentException("used files request has no step " + step); throw helper.cancel(new ERR(), e, "bad-step"); } } catch (Cancel c) { throw c; } catch (Throwable t) { throw helper.cancel(new ERR(), t, "used-files-fail"); } } @Override public void finish() { if (reader != null) { try { reader.close(); } catch (IOException e) { throw helper.cancel(new ERR(), e, "used-files-fail"); } finally { reader = null; } } } @Override protected void finalize() { finish(); } @Override public void buildResponse(int step, Object object) { helper.assertResponse(step); if (step == 0) { helper.setResponseIfNull((Response) object); } } @Override public Response getResponse() { return helper.getResponse(); } /* ACTUALLY QUERY FOR USED FILES AND CONSTRUCT RESPONSE */ /** * Set up private fields and generate appropriate response. * @return the response to the request * @throws Cancel if a response cannot be provided */ private Response determineResponse() throws Cancel { final String hql = "SELECT fileset.id FROM Image WHERE id = :id"; final Query query = session.createQuery(hql).setParameter("id", imageId); @SuppressWarnings("unchecked") final List<Object> results = query.list(); if (results.isEmpty()) { final Exception e = new IllegalArgumentException("cannot read image " + imageId); throw helper.cancel(new ERR(), e, "bad-image"); } filesetId = (Long) results.get(0); try { findPixels(); } catch (Cancel c) { throw c; } catch (Throwable t) { throw helper.cancel(new ERR(), t, "used-files-fail"); } return filesetId == null ? determineResponsePreFs() : determineResponseFs(); } /** * Find the image's primary pixels object and, if an FS image, open a Bio-Formats reader for it. * @throws Cancel if there are no pixels for the image * @throws FormatException if the pixels service could not open a Bio-Formats reader for the image * @throws IOException if the pixels service could not open a Bio-Formats reader for the image */ private void findPixels() throws Cancel, FormatException, IOException { final String hql = "FROM Image WHERE id = :id"; final Query query = session.createQuery(hql).setParameter("id", imageId); final Image image = (Image) query.uniqueResult(); if (image.sizeOfPixels() < 1) { final Exception e = new IllegalArgumentException("no pixels for image " + imageId); throw helper.cancel(new ERR(), e, "bad-image"); } final Pixels pixels = image.getPrimaryPixels(); pixelsId = pixels.getId(); if (filesetId != null) { reader = pixelsService.getBfReader(pixels); } } /** * @param repo a repository identifier * @return the root of the repository on the filesystem */ private File getRepositoryRoot(String repo) { File root = repositoryRoots.get(repo); if (root == null) { final String hql = "SELECT path || name FROM OriginalFile WHERE mimetype = 'Repository' AND hash = :repo"; final Query query = session.createQuery(hql).setParameter("repo", repo); final String repositoryRootDirectory = (String) query.uniqueResult(); root = new File(repositoryRootDirectory); repositoryRoots.put(repo, root); } return root; } /** * @param repo a repository * @param file a file path in the repository * @return the path of the file on the filesystem */ private File getUnderlyingFile(String repo, String file) { /* note: cannot use ServerFilePathTransformer from within a Request */ final File repoRoot = getRepositoryRoot(repo); if ('/' != File.separatorChar) { file = file.replace('/', File.separatorChar); } return new File(repoRoot, file); } /** * @return the paths and IDs of the files in the FS image's fileset */ private Map<File, Long> getFileIdsOfFileset() { /* useful for translating filenames from Bio-Formats to IDs of OriginalFile objects */ final SqlAction jdbc = helper.getSql(); final Map<File, Long> fileIds = new HashMap<File, Long>(); final String hql = "SELECT originalFile.id, originalFile.path || originalFile.name FROM FilesetEntry " + "WHERE fileset.id = :id"; final Query query = session.createQuery(hql).setParameter("id", filesetId); @SuppressWarnings("unchecked") final List<Object[]> results = query.list(); for (final Object[] result : results) { final Long fileId = (Long) result[0]; final String fileName = (String) result[1]; final String repo = jdbc.fileRepo(fileId); final File realFile = getUnderlyingFile(repo, fileName); fileIds.put(realFile, fileId); } return fileIds; } /** * @return the response to the request, for FS images */ private UsedFilesResponse determineResponseFs() { final Map<File, Long> fileIds = getFileIdsOfFileset(); final Set<File> allFileNamesAllSeries = namesToFiles(reader.getUsedFiles(false)); final Set<File> companionFileNamesAllSeries = namesToFiles(reader.getUsedFiles(true)); final Set<File> binaryFileNamesAllSeries = Sets.difference(allFileNamesAllSeries, companionFileNamesAllSeries); final Set<File> allFileNamesThisSeries = namesToFiles(reader.getSeriesUsedFiles(false)); final Set<File> companionFileNamesThisSeries = namesToFiles(reader.getSeriesUsedFiles(true)); final Set<File> binaryFileNamesThisSeries = Sets.difference(allFileNamesThisSeries, companionFileNamesThisSeries); final Set<File> companionFileNamesOtherSeries = Sets.difference(companionFileNamesAllSeries, companionFileNamesThisSeries); final Set<File> binaryFileNamesOtherSeries = Sets.difference(binaryFileNamesAllSeries, binaryFileNamesThisSeries); final List<Long> binaryFileIdsThisSeries = mapList(fileIds, binaryFileNamesThisSeries); final List<Long> binaryFileIdsOtherSeries = mapList(fileIds, binaryFileNamesOtherSeries); final List<Long> companionFileIdsThisSeries = mapList(fileIds, companionFileNamesThisSeries); final List<Long> companionFileIdsOtherSeries = mapList(fileIds, companionFileNamesOtherSeries); return new UsedFilesResponse(binaryFileIdsThisSeries, binaryFileIdsOtherSeries, companionFileIdsThisSeries, companionFileIdsOtherSeries); } /** * @return the IDs of the pre-FS image's archived files */ private List<Long> getArchivedFilesPreFs() { final String hql = "SELECT DISTINCT parent.id FROM PixelsOriginalFileMap WHERE child.id = :id"; final Query query = session.createQuery(hql).setParameter("id", pixelsId); return objectsToLongs(query.list()); } /** * Note the companion files for a given set of model objects. * @param fileNames where to note the IDs and names of the companion files attached to the given model objects * @param type the simple name of the type of model object whose companion files are to be noted * @param ids the IDs of the model objects whose companion files are to be noted */ private void getCompanionFilesPreFs(Map<Long, String> fileNames, String type, Collection<Long> ids) { if (ids.isEmpty()) { return; } final String hql = "SELECT DISTINCT fa.file.id, fa.file.name FROM " + type + "AnnotationLink al, FileAnnotation fa " + "WHERE al.parent.id IN (:ids) AND al.child = fa AND fa.ns = :ns"; final Query query = session.createQuery(hql).setParameterList("ids", ids).setParameter("ns", NSCOMPANIONFILE.value); @SuppressWarnings("unchecked") final List<Object[]> results = query.list(); for (final Object[] result : results) { final Long fileId = (Long) result[0]; final String fileName = (String) result[1]; fileNames.put(fileId, fileName); } } /** * For a set of model object IDs, determine the IDs of a related set of model objects. * @param hql the HQL for finding the related object IDs * @param ids IDs of model objects * @return IDs of related model objects */ private Set<Long> queryMapping(String hql, Collection<Long> ids) { if (ids.isEmpty()) { return Collections.emptySet(); } final Set<Long> found = new HashSet<Long>(); final Query query = session.createQuery(hql).setParameterList("ids", ids); @SuppressWarnings("unchecked") final List<Object> results = query.list(); for (final Object result : results) { if (result != null) { found.add((Long) result); } } return found; } /** * @return the IDs and names of the pre-FS image's companion files */ private Map<Long, String> getCompanionFilesPreFs() { final Map<Long, String> fileNames = new HashMap<Long, String>(); final Set<Long> images = Collections.singleton(imageId); final Set<Long> wellSamples = queryMapping("SELECT DISTINCT id FROM WellSample WHERE image.id IN (:ids)", images); final Set<Long> wells = queryMapping("SELECT DISTINCT well.id FROM WellSample WHERE id IN (:ids)", wellSamples); final Set<Long> runs = queryMapping("SELECT DISTINCT plateAcquisition.id FROM WellSample WHERE id IN (:ids)", wellSamples); final Set<Long> plates = queryMapping("SELECT DISTINCT plate.id FROM Well WHERE id IN (:ids)", wells); getCompanionFilesPreFs(fileNames, "Image", images); getCompanionFilesPreFs(fileNames, "Well", wells); getCompanionFilesPreFs(fileNames, "PlateAcquisition", runs); getCompanionFilesPreFs(fileNames, "Plate", plates); return fileNames; } /** * @return the response to the request, for non-FS images */ private UsedFilesResponsePreFs determineResponsePreFs() { final List<Long> archivedFiles = getArchivedFilesPreFs(); final List<Long> realCompanionFiles = new ArrayList<Long>(); final List<Long> originalMetadataFiles = new ArrayList<Long>(); for (final Map.Entry<Long, String> companionFile : getCompanionFilesPreFs().entrySet()) { final Long id = companionFile.getKey(); final String name = companionFile.getValue(); (ORIGINALMETADATA.value.equals(name) ? originalMetadataFiles : realCompanionFiles).add(id); } return new UsedFilesResponsePreFs(archivedFiles, realCompanionFiles, originalMetadataFiles); } /** * Convert {@code Collection<?>} to {@code List<Long>}. * @param objects a collection of {@link Long}s * @return a list of the same {@link Long}s */ private static List<Long> objectsToLongs(Collection<?> objects) { final List<Long> longs = new ArrayList<Long>(objects.size()); for (final Object object : objects) { longs.add((Long) object); } return longs; } /** * Convert filenames to {@link File} instances. * @param names filenames * @return the corresponding {@link File} objects, or an empty list if passed {@code null} */ private static Set<File> namesToFiles(String... names) { final Set<File> files = new HashSet<File>(); if (names != null) { for (final String name : names) { files.add(new File(name)); } } return files; } /** * Apply a mapping function to a collection of items. * @param mapping the mapping function to apply * @param input a collection of items * @return the map value for each item, in order */ private static <X, Y> List<Y> mapList(Map<X, Y> mapping, Collection<X> input) { final List<Y> output = new ArrayList<Y>(input.size()); for (final X item : input) { output.add(mapping.get(item)); } return output; } }