OmeroBrowser.java

package com.github.celldynamics.quimp.omero;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

import org.apache.commons.io.FilenameUtils;
import org.scijava.Context;
import org.scijava.io.IOService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.QuimpException;

import Glacier2.CannotCreateSessionException;
import Glacier2.PermissionDeniedException;
import edu.emory.mathcs.backport.java.util.Collections;
import loci.formats.in.DefaultMetadataOptions;
import loci.formats.in.MetadataLevel;
import net.imagej.Dataset;
import net.imagej.omero.DefaultOMEROSession;
import net.imagej.omero.OMEROLocation;
import net.imagej.omero.OMEROService;
import ome.formats.OMEROMetadataStoreClient;
import ome.formats.importer.ImportCandidates;
import ome.formats.importer.ImportConfig;
import ome.formats.importer.ImportLibrary;
import ome.formats.importer.OMEROWrapper;
import ome.formats.importer.cli.ErrorHandler;
import ome.formats.importer.cli.LoggingImportMonitor;
import omero.ServerError;
import omero.client;
import omero.api.IMetadataPrx;
import omero.api.RawFileStorePrx;
import omero.gateway.Gateway;
import omero.gateway.SecurityContext;
import omero.gateway.exception.DSAccessException;
import omero.gateway.exception.DSOutOfServiceException;
import omero.gateway.facility.BrowseFacility;
import omero.gateway.facility.DataManagerFacility;
import omero.gateway.model.DatasetData;
import omero.gateway.model.FileAnnotationData;
import omero.gateway.model.ImageData;
import omero.gateway.model.ProjectData;
import omero.model.Annotation;
import omero.model.ChecksumAlgorithm;
import omero.model.ChecksumAlgorithmI;
import omero.model.FileAnnotation;
import omero.model.FileAnnotationI;
import omero.model.Image;
import omero.model.ImageAnnotationLink;
import omero.model.ImageAnnotationLinkI;
import omero.model.OriginalFile;
import omero.model.OriginalFileI;
import omero.model.enums.ChecksumAlgorithmSHA1160;
import omero.sys.ParametersI;

/**
 * Provide access to Omero.
 * 
 * @author p.baniukiewicz
 *
 */
public class OmeroBrowser implements Closeable {
  private static final String NAMESPACE = "QCONF";
  static final Logger LOGGER = LoggerFactory.getLogger(OmeroBrowser.class.getName());
  static final int DEF_PORT = 4064;

  private Gateway gateway = null;
  private SecurityContext ctx;
  private String user;
  private String host;
  private String pass;
  private client client;
  private DefaultOMEROSession ds;
  OMEROService os;
  Context context;
  private int port;

  /**
   * Initialise browser.
   * 
   * <p>Do not open connection. Use {@link #connect()} instead.
   * 
   * @param user omero user name
   * @param pass omero password
   * @param host host
   * @param port port, see {@link #DEF_PORT}
   */
  public OmeroBrowser(String user, String pass, String host, int port) {
    this.port = port;
    this.host = host;
    this.user = user;
    this.pass = pass;
  }

  /**
   * Connect to Omero.
   * 
   * <p>All fields provided to {@link #OmeroBrowser(String, String, String, int)} must contain
   * nonempty strings. Throw exception if connection unsuccessful.
   * 
   * @throws DSOutOfServiceException connection error
   * @throws QuimpException if any of fields: user/pass/host is empty or null
   * @throws URISyntaxException on Omero error
   * @throws CannotCreateSessionException on Omero error
   * @throws PermissionDeniedException on Omero error
   * @throws ServerError on Omero error
   */
  public void connect() throws DSOutOfServiceException, QuimpException, URISyntaxException,
          ServerError, PermissionDeniedException, CannotCreateSessionException {
    silentClose(); // close old connection
    LOGGER.debug("Opening connection:" + user + ", " + pass + ", " + host);
    if (user == null || user.isEmpty() || host == null || host.isEmpty() || pass == null
            || pass.isEmpty()) {
      throw new QuimpException("One of required fields is empty");
    }
    OMEROLocation credentials = new OMEROLocation(host, port, user, pass);
    context = new Context();
    os = context.getService(OMEROService.class);
    ds = new DefaultOMEROSession(credentials, os);
    client = ds.getClient();
    gateway = ds.getGateway();
    ctx = ds.getSecurityContext();
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.io.Closeable#close()
   */
  @Override
  public void close() throws IOException {
    silentClose();
  }

  /**
   * Close Omero without exception.
   */
  public void silentClose() {
    if (gateway != null) {
      ds.close();
      client.closeSession();
      gateway.disconnect();
      os.dispose();
      context.dispose();
      LOGGER.debug("Omero disconnected");
    }
  }

  /**
   * List projects.
   * 
   * @return list of projects for user.
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   * @throws ExecutionException on Omero error
   */
  public List<ProjectData> listProjects()
          throws DSOutOfServiceException, DSAccessException, ExecutionException {
    BrowseFacility browse = gateway.getFacility(BrowseFacility.class);
    return new ArrayList<ProjectData>(browse.getProjects(ctx));
  }

  /**
   * List datasets.
   * 
   * @return List of datasets for user.
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   * @throws ExecutionException on Omero error
   */
  public List<DatasetData> listDatasets()
          throws DSOutOfServiceException, DSAccessException, ExecutionException {
    BrowseFacility browse = gateway.getFacility(BrowseFacility.class);
    return new ArrayList<DatasetData>(browse.getDatasets(ctx));
  }

  /**
   * Open dataset.
   * 
   * @param name dataset name
   * @return images from dataset
   * @throws ExecutionException on Omero error
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   */
  public Collection<ImageData> openDataset(DatasetData name)
          throws ExecutionException, DSOutOfServiceException, DSAccessException {
    BrowseFacility browse = gateway.getFacility(BrowseFacility.class);
    List<Long> ida = new ArrayList<>();
    ida.add(name.getId());
    return browse.getImagesForDatasets(ctx, ida);

  }

  /**
   * Find dataset of specified name.
   * 
   * @param name name of dataset
   * @return dataset object
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   * @throws ExecutionException on Omero error
   */
  public DatasetData findDataset(String name)
          throws DSOutOfServiceException, DSAccessException, ExecutionException {
    for (DatasetData ds : listDatasets()) {
      if (ds.getName().equals(name)) {
        LOGGER.debug("Found dataset of name " + name + " (" + ds.toString() + ")");
        return ds;
      }
    }
    return null;
  }

  /**
   * Find dataset of specified id.
   * 
   * @param id id of dataset
   * @return dataset object
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   * @throws ExecutionException on Omero error
   */
  public DatasetData findDataset(Long id)
          throws DSOutOfServiceException, DSAccessException, ExecutionException {
    for (DatasetData ds : listDatasets()) {
      if (ds.getId() == id) {
        LOGGER.debug("Found dataset of id " + id + " (" + ds.toString() + ")");
        return ds;
      }
    }
    return null;
  }

  /**
   * Find image of specified name in the dataset.
   * 
   * <p>If there is more than 1 image with specified name, then depending on
   * <tt>allowDuplicates</tt> the newest will be returned (true) or exception thrown (false).
   * 
   * @param imageName image name to look for
   * @param name dataset name
   * @param allowDuplicates if true newest image is returned if there is more than 1 image with
   *        imageName in the dataset, otherwise exception is thrown
   * @return found image object
   * 
   * @throws ExecutionException omero error
   * @throws DSOutOfServiceException omero error
   * @throws DSAccessException omero error
   * @throws QuimpException if there are multiple images with the same name and
   *         allowDuplicates==false
   */
  public ImageData findImage(String imageName, DatasetData name, boolean allowDuplicates)
          throws ExecutionException, DSOutOfServiceException, DSAccessException, QuimpException {
    ArrayList<ImageData> imgs = new ArrayList<>();
    for (ImageData im : openDataset(name)) {
      if (im.getName().equals(imageName)) {
        LOGGER.debug("Found image of name " + name + " (" + im.toString() + "), inserted: "
                + im.getInserted());
        imgs.add(im);
      }
    }
    if (imgs.size() > 1 && allowDuplicates == false) {
      throw new QuimpException(
              "There are at least two images with name " + imageName + " in dataset");
    }
    if (imgs.size() == 0) {
      return null;
    }
    if (imgs.size() == 1) {
      return imgs.get(0);
    } else { // return newest
      Long now = System.currentTimeMillis(); // current time
      ArrayList<Long> deltas = new ArrayList<>(); // time from now for each file (with same name)
      for (ImageData im : imgs) { // check each file
        Long timeFile = im.getInserted().getTime(); // get upload time
        deltas.add(now - timeFile); // store difference from now
      }
      int closestInd = deltas.indexOf(Collections.min(deltas)); // index of smallest diff
      return imgs.get(closestInd); // file under this index
    }

  }

  /**
   * Find image of specified id in dataset.
   * 
   * @param imageId id to find
   * @param name dataset name
   * @return image object
   * @throws ExecutionException on Omero error
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   */
  public ImageData findImage(Long imageId, DatasetData name)
          throws ExecutionException, DSOutOfServiceException, DSAccessException {
    for (ImageData im : openDataset(name)) {
      if (im.getId() == imageId) {
        LOGGER.debug("Found image of id " + imageId + " (" + imageId.toString() + ")");
        return im;
      }
    }
    return null;

  }

  /**
   * Upload specified image to dataset.
   * 
   * @param pathsToImages list paths of images
   * @param name dataset name
   * @throws Exception on error
   */
  public void upload(String[] pathsToImages, DatasetData name) throws Exception {
    LOGGER.info("Trying to upload: " + Arrays.toString(pathsToImages));
    ImportConfig config = new ome.formats.importer.ImportConfig();
    config.email.set("");
    config.sendFiles.set(true);
    config.sendReport.set(false);
    config.contOnError.set(false);
    config.debug.set(true);

    config.hostname.set(host);
    config.port.set(port);
    config.username.set(user);
    config.password.set(pass);
    config.target.set("omero.model.Dataset:" + name.getId());

    OMEROMetadataStoreClient store;
    store = config.createStore();
    store.logVersionInfo(config.getIniVersionNumber());
    OMEROWrapper reader = new OMEROWrapper(config);
    ImportLibrary library = new ImportLibrary(store, reader);

    ErrorHandler handler = new ErrorHandler(config);
    library.addObserver(new LoggingImportMonitor());

    ImportCandidates candidates = new ImportCandidates(reader, pathsToImages, handler);
    reader.setMetadataOptions(new DefaultMetadataOptions(MetadataLevel.ALL));
    library.importCandidates(config, candidates);

    store.logout();
    LOGGER.info("Image uploaded");

  }

  /**
   * Upload attachment and attach it to image from dataset.
   * 
   * <p>Image is already in dataset.
   * 
   * @param imageName image name in dataset
   * @param pathToAttach path to attachment
   * @param name dataset name
   * @throws IOException on file error
   * @throws DSOutOfServiceException on Omero error
   * @throws DSAccessException on Omero error
   * @throws ServerError on Omero error
   * @throws ExecutionException on Omero error
   * @throws QuimpException on Omero error - no image in dataset
   */
  public void upload(String imageName, String pathToAttach, DatasetData name)
          throws IOException, DSOutOfServiceException, DSAccessException, ServerError,
          ExecutionException, QuimpException {
    LOGGER.info("Trying to upload attachement: " + pathToAttach + " to " + imageName);

    File file = new File(pathToAttach);
    String name1 = Paths.get(pathToAttach).getFileName().toString();
    String absolutePath = file.getAbsolutePath();
    String path = absolutePath.substring(0, absolutePath.length() - name1.length());

    // create the original file object.
    OriginalFile originalFile = new OriginalFileI();
    originalFile.setName(omero.rtypes.rstring(name1));
    originalFile.setPath(omero.rtypes.rstring(path));
    originalFile.setSize(omero.rtypes.rlong(file.length()));
    final ChecksumAlgorithm checksumAlgorithm = new ChecksumAlgorithmI();
    checksumAlgorithm.setValue(omero.rtypes.rstring(ChecksumAlgorithmSHA1160.value));
    originalFile.setHasher(checksumAlgorithm);
    originalFile.setMimetype(omero.rtypes.rstring("application/octet-stream"));
    // Now we save the originalFile object
    DataManagerFacility dm = gateway.getFacility(DataManagerFacility.class);
    originalFile = (OriginalFile) dm.saveAndReturnObject(ctx, originalFile);

    // Initialize the service to load the raw data
    RawFileStorePrx rawFileStore = gateway.getRawFileService(ctx);

    long inc = file.length();
    long pos = 0;
    int rlen;
    byte[] buf = new byte[(int) inc];
    ByteBuffer bbuf;
    // Open file and read stream
    try (FileInputStream stream = new FileInputStream(file)) {
      rawFileStore.setFileId(originalFile.getId().getValue());
      while ((rlen = stream.read(buf)) > 0) {
        rawFileStore.write(buf, pos, rlen);
        pos += rlen;
        bbuf = ByteBuffer.wrap(buf);
        bbuf.limit(rlen);
      }
      originalFile = rawFileStore.save();
    } finally {
      rawFileStore.close();
    }
    // now we have an original File in DB and raw data uploaded.
    // We now need to link the Original file to the image using
    // the File annotation object. That's the way to do it.
    FileAnnotation fa = new FileAnnotationI();
    fa.setFile(originalFile);
    // The description set above e.g. PointsModel
    fa.setDescription(omero.rtypes.rstring("PointsModel"));
    // The name space you have set to identify the file annotation.
    fa.setNs(omero.rtypes.rstring(NAMESPACE));

    // save the file annotation.
    fa = (FileAnnotation) dm.saveAndReturnObject(ctx, fa);

    // now link the image and the annotation
    ImageAnnotationLink link = new ImageAnnotationLinkI();
    link.setChild(fa);
    ImageData imageData = findImage(imageName, name, true);
    link.setParent(imageData.asImage());
    // save the link back to the server.
    link = (ImageAnnotationLink) dm.saveAndReturnObject(ctx, link);
    // o attach to a Dataset use DatasetAnnotationLink;
    LOGGER.info("Attachement uploaded");

  }

  /**
   * Download image and attachment.
   * 
   * @param image image with attachment (if no attachment only image is downloaded)
   * @param path path to file (should be not file name)
   * @throws DSOutOfServiceException on Omero error
   * @throws ServerError on Omero error
   * @throws DSAccessException on Omero error
   * @throws ExecutionException on Omero error
   * @throws IOException on file error
   * @throws URISyntaxException on Omero error
   * @throws PermissionDeniedException on Omero error
   * @throws CannotCreateSessionException on Omero error
   */
  public void download(ImageData image, Path path)
          throws DSOutOfServiceException, ServerError, DSAccessException, ExecutionException,
          IOException, URISyntaxException, PermissionDeniedException, CannotCreateSessionException {

    if (path.toFile().isFile()) {
      throw new IllegalArgumentException("Path must point to folder");
    }

    Dataset img = os.downloadImage(client, image.getId());
    IOService io = context.getService(IOService.class);
    Path imageFile = path.resolve(image.getName());
    io.save(img, imageFile.toString());
    long[] dim = new long[img.numDimensions()];
    img.dimensions(dim);
    LOGGER.info("Saved: " + imageFile.toString() + " : " + img.toString() + " dim: "
            + Arrays.toString(dim));

    long userId = gateway.getLoggedInUser().getId();
    List<String> nsToInclude = new ArrayList<String>();
    nsToInclude.add(NAMESPACE);
    List<String> nsToExclude = new ArrayList<String>();
    ParametersI param = new ParametersI();
    param.exp(omero.rtypes.rlong(userId)); // load the annotation for a given user.
    IMetadataPrx proxy = gateway.getMetadataService(ctx);
    List<Long> ids = new ArrayList<>();
    ids.add(image.getId());
    Map<Long, List<Annotation>> annotations =
            proxy.loadSpecifiedAnnotationsLinkedTo(FileAnnotation.class.getName(), nsToInclude,
                    nsToExclude, Image.class.getName(), ids, param);
    LOGGER.debug("Length: " + annotations.size());
    // for (Annotation a : annotations.get(image.getId())) {
    // if (a instanceof FileAnnotation) {
    // FileAnnotationData fa = new FileAnnotationData((FileAnnotation) a);
    // LOGGER.debug("id " + fa.getFileID());
    // }
    // }

    if (annotations.get(image.getId()) != null) {
      Iterator<Annotation> j = annotations.get(image.getId()).iterator();
      Annotation annotation;
      FileAnnotationData fa;
      RawFileStorePrx store = gateway.getRawFileService(ctx);
      File qconfFile =
              path.resolve(FilenameUtils.removeExtension(image.getName()) + ".QCONF").toFile();
      int index = 0;

      int inc;
      try (FileOutputStream stream = new FileOutputStream(qconfFile)) {
        while (j.hasNext()) {
          annotation = j.next();
          if (annotation instanceof FileAnnotation && index == 0) {
            fa = new FileAnnotationData((FileAnnotation) annotation);
            // // The id of the original file
            // create the original file object.
            OriginalFile originalFile = new OriginalFileI();
            originalFile.setName(omero.rtypes.rstring(qconfFile.getName()));
            originalFile.setPath(omero.rtypes.rstring(qconfFile.getParent()));
            originalFile.setSize(omero.rtypes.rlong(fa.getFileSize()));
            final ChecksumAlgorithm checksumAlgorithm = new ChecksumAlgorithmI();
            checksumAlgorithm.setValue(omero.rtypes.rstring(ChecksumAlgorithmSHA1160.value));
            originalFile.setHasher(checksumAlgorithm);
            originalFile.setMimetype(omero.rtypes.rstring("application/octet-stream"));
            // Now we save the originalFile object
            DataManagerFacility dm = gateway.getFacility(DataManagerFacility.class);
            originalFile = (OriginalFile) dm.saveAndReturnObject(ctx, originalFile);

            store.setFileId(fa.getFileID());
            int offset = 0;
            inc = (int) fa.getFileSize();
            long size = originalFile.getSize().getValue();
            try {
              for (offset = 0; (offset + inc) < size;) {
                stream.write(store.read(offset, inc));
                offset += inc;
              }
            } finally {
              stream.write(store.read(offset, (int) (size - offset)));
              LOGGER.info("Saved: " + qconfFile.toString() + " from image " + image.getId());
            }
            index++;
          }
        }
      } finally {
        store.close();
      }
    } else {
      LOGGER.warn("There is no QCONF file attached to image [ " + image.getId() + "]");
    }
  }

  /**
   * Dummy tests.
   * 
   * @param args args
   */
  public static void main(String[] args) {
    String user;
    String pass;
    String host;
    Properties prop = new Properties();
    try (FileInputStream input = new FileInputStream(
            Paths.get(System.getProperty("user.home"), "omero.properties").toFile())) {
      prop.load(input);
      user = prop.getProperty("user");
      pass = prop.getProperty("pass");
      host = prop.getProperty("host");
    } catch (IOException ex) {
      ex.printStackTrace();
      return;
    }

    try (OmeroBrowser client = new OmeroBrowser(user, pass, host, DEF_PORT)) {
      client.connect();
      LOGGER.debug("Projects:");
      client.listDatasets().stream().forEach(x -> LOGGER.debug("dataset " + x.getName()));
      // pick dataset
      DatasetData ds = client.findDataset("Q");
      // list images
      client.openDataset(ds).stream()
              .forEach(x -> LOGGER.debug("image: " + x.getName() + ": " + x.getId()));

      // client.upload(new String[] {
      // "src/test/Resources-static/ProtAnalysisTest/fluoreszenz-test.tif kept stack.tif" }, ds);
      // client.upload("fluoreszenz-test.tif kept stack.tif",
      // "src/test/Resources-static/ProtAnalysisTest/fluoreszenz-test.QCONF", ds);
      client.download(client.findImage(194021L, ds), Paths.get("./").toAbsolutePath());

      // client.downloadIJ_TEST(); // this or these above

    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }

    // https://github.com/ome/minimal-omero-client/blob/master/src/main/java/com/example/SimpleConnection.java
    // https://docs.openmicroscopy.org/omero/5.4.10/developers/Java.html

  }

}