// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.service.cherrypicks;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Serializable;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.Comparator;
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.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.collections.Factory;
import org.apache.commons.collections.MultiMap;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.log4j.Logger;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import edu.harvard.med.screensaver.db.GenericEntityDAO;
import edu.harvard.med.screensaver.db.LibrariesDAO;
import edu.harvard.med.screensaver.model.VolumeUnit;
import edu.harvard.med.screensaver.model.cherrypicks.CherryPickAssayPlate;
import edu.harvard.med.screensaver.model.cherrypicks.CherryPickRequest;
import edu.harvard.med.screensaver.model.cherrypicks.LabCherryPick;
import edu.harvard.med.screensaver.model.cherrypicks.LabCherryPick.LabCherryPickStatus;
import edu.harvard.med.screensaver.model.libraries.Copy;
import edu.harvard.med.screensaver.model.libraries.Plate;
import edu.harvard.med.screensaver.model.libraries.PlateType;
import edu.harvard.med.screensaver.util.CSVPrintWriter;
import edu.harvard.med.screensaver.util.CustomNewlinePrintWriter;
import edu.harvard.med.screensaver.util.Pair;
/**
* For a cherry pick request, builds the CSV files that define the assay plate
* mapping.
*
* @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a>
* @author <a mailto="john_sullivan@hms.harvard.edu">John Sullivan</a>
*/
public class CherryPickRequestPlateMapFilesBuilder
{
/**
* File extension for each plate map file in the zip file.
* Stewart Rudnicki has indicated that this file extension should lower-case..
*/
private static final String PLATE_MAP_FILE_EXTENSION = ".csv";
/**
* Windows newline must be used! The consumer of the files we're generating is
* a machine in the lab that has this requirement, so cross-platform concerns
* are not a concern.
*/
private static final String NEWLINE = "\r\n";
private static final String[] CSV_FILE_HEADERS = {
"Source Plate",
"Source Copy",
"Source Well",
"Source Plate Type",
"Destination Well",
"Destination Plate Type",
"Person Visiting",
"Screen Number",
"Volume"};
private static final String[] DISTINCT_PLATECOPYLIST_FILE_HEADERS = {
"Source Plate",
"Source Copy",
"Location" };
private static final String README_FILE_NAME = "README.txt";
private static final String DISTINCT_PLATE_COPY_LIST_FILE_NAME = "plate-copy-location.csv";
// static members
private static Logger log = Logger.getLogger(CherryPickRequestPlateMapFilesBuilder.class);
// instance data members
private GenericEntityDAO genericEntityDao;
private CherryPickRequestPlateMapper cherryPickRequestPlateMapper;
private LibrariesDAO librariesDao;
// public constructors and methods
public CherryPickRequestPlateMapFilesBuilder(GenericEntityDAO dao,
LibrariesDAO librariesDao,
CherryPickRequestPlateMapper cherryPickRequestPlateMapper)
{
this.genericEntityDao = dao;
this.librariesDao = librariesDao;
this.cherryPickRequestPlateMapper = cherryPickRequestPlateMapper;
}
// public constructors and methods
public InputStream buildZip(final CherryPickRequest cherryPickRequestIn,
final Set<CherryPickAssayPlate> plates) throws IOException
{
CherryPickRequest cherryPickRequest = (CherryPickRequest) genericEntityDao.reattachEntity(cherryPickRequestIn);
return doBuildZip(cherryPickRequest, plates);
}
// private methods
@SuppressWarnings("unchecked")
private InputStream doBuildZip(CherryPickRequest cherryPickRequest,
Set<CherryPickAssayPlate> forPlates) throws IOException
{
ByteArrayOutputStream zipOutRaw = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(zipOutRaw);
MultiMap/*<String,SortedSet<CherryPick>>*/ files2CherryPicks = buildCherryPickFiles(cherryPickRequest, forPlates);
buildReadme(cherryPickRequest, zipOut);
buildDistinctPlateCopyFile(cherryPickRequest, forPlates, zipOut);
PrintWriter out = new CSVPrintWriter(new OutputStreamWriter(zipOut), NEWLINE);
for (Iterator iter = files2CherryPicks.keySet().iterator(); iter.hasNext();) {
String fileName = (String) iter.next();
ZipEntry zipEntry = new ZipEntry(fileName);
zipOut.putNextEntry(zipEntry);
writeHeadersRow(out);
for (LabCherryPick cherryPick : (SortedSet<LabCherryPick>) files2CherryPicks.get(fileName)) {
writeCherryPickRow(out, cherryPick);
}
out.flush();
}
out.close();
return new ByteArrayInputStream(zipOutRaw.toByteArray());
}
@SuppressWarnings("unchecked")
private void buildReadme(CherryPickRequest cherryPickRequest,
ZipOutputStream zipOut)
throws IOException
{
ZipEntry zipEntry = new ZipEntry(README_FILE_NAME);
zipOut.putNextEntry(zipEntry);
PrintWriter writer = new CustomNewlinePrintWriter(zipOut, NEWLINE);
writer.println("This zip file contains plate mappings for Cherry Pick Request " +
cherryPickRequest.getCherryPickRequestNumber());
writer.println();
{
StringBuilder buf = new StringBuilder();
for (CherryPickAssayPlate assayPlate : cherryPickRequest.getActiveCherryPickAssayPlates()) {
buf.
append(assayPlate.getName()).
append("\t").append(assayPlate.getStatusLabel());
if (assayPlate.isPlatedAndScreened()) {
buf.append("\t(").
append(assayPlate.getCherryPickScreenings().last().getDateOfActivity()).
append(" by ").
append(assayPlate.getCherryPickScreenings().last().getPerformedBy().getFullNameFirstLast()).
append(')');
}
else if (assayPlate.isPlated()) {
buf.append("\t(").
append(assayPlate.getCherryPickLiquidTransfer().getDateOfActivity()).
append(" by ").
append(assayPlate.getCherryPickLiquidTransfer().getPerformedBy().getFullNameFirstLast()).
append(')');
}
buf.append(NEWLINE);
}
if (buf.length() > 0) {
writer.println("Cherry pick plates:");
writer.print(buf.toString());
writer.println();
}
}
Map<CherryPickAssayPlate,Integer> platesRequiringReload =
cherryPickRequestPlateMapper.getAssayPlatesRequiringSourcePlateReload(cherryPickRequest);
if (platesRequiringReload.size() > 0) {
writer.println("WARNING: Some cherry pick plates will be created from the same source plate!");
writer.println("You will need to reload one or more source plates for each of the following cherry pick plates:");
for (CherryPickAssayPlate assayPlate : platesRequiringReload.keySet()) {
writer.println("\tCherry pick plate '" + assayPlate.getName() +
"' requires reload of source plate " + platesRequiringReload.get(assayPlate));
}
writer.println();
}
{
StringBuilder buf = new StringBuilder();
MultiMap sourcePlateTypesForEachAssayPlate = getSourcePlateTypesForEachAssayPlate(cherryPickRequest);
for (CherryPickAssayPlate assayPlate : cherryPickRequest.getActiveCherryPickAssayPlates()) {
Set<PlateType> sourcePlateTypes = (Set<PlateType>) sourcePlateTypesForEachAssayPlate.get(assayPlate.getName());
if (sourcePlateTypes != null && sourcePlateTypes.size() > 1) {
buf.append(assayPlate.getName()).append(NEWLINE);
}
}
if (buf.length() > 0) {
writer.println("WARNING: Some cherry pick plates will be created from multiple source plates of non-uniform plate types!");
writer.println("The following cherry pick plates are specified across multiple files:");
writer.print(buf.toString());
writer.println();
}
}
writer.flush();
}
/**
* for [#3206] Generate a distinct plate/copy list for CPR download file
*/
private void buildDistinctPlateCopyFile(CherryPickRequest cherryPickRequest,
Set<CherryPickAssayPlate> forPlates,
ZipOutputStream zipOut) throws IOException
{
PrintWriter out = new CSVPrintWriter(new OutputStreamWriter(zipOut), NEWLINE);
zipOut.putNextEntry(new ZipEntry(DISTINCT_PLATE_COPY_LIST_FILE_NAME));
for (String string : DISTINCT_PLATECOPYLIST_FILE_HEADERS) {
out.print(string);
}
out.println();
// TODO: consider caching this while building the CPR
Set<Plate> sourcePlates = Sets.newHashSet();
for(CherryPickAssayPlate plate: forPlates)
{
for(LabCherryPick lcp:plate.getLabCherryPicks())
{
lcp = genericEntityDao.reloadEntity(lcp, true);
sourcePlates.add(librariesDao.findPlate(lcp.getSourceWell().getPlateNumber(), lcp.getSourceCopy().getName()));
}
}
// sort by plate/copy/location
List<Plate> list = Lists.newArrayList(sourcePlates);
Collections.sort(list, new Comparator<Plate>() {
@Override
public int compare(Plate o1, Plate o2)
{
if(o1.equals(o2))
{
return o1.getLocation().compareTo(o2.getLocation());
}
return o1.compareTo(o2); // note Plate.compare() does a plate/copy compare
}});
for(Plate p:list)
{
out.print(p.getPlateNumber());
out.print(p.getCopy().getName());
out.print(p.getLocation() == null ? "" : p.getLocation().toDisplayString());
out.println();
}
}
@SuppressWarnings("unchecked")
/**
* Normally, we create 1 file per assay plate. However, in the case where an
* assay plate is comprised of wells from library copy plates that have
* different plate types, we need to generate a separate file for each source
* plate type (i.e., the assay plate will be defined over multiple files).
* @return a MultiMap that partitions the cherry picks by file,
* ordering both the file names and cherry picks for each file.
*/
private MultiMap/*<String,SortedSet<CherryPick>>*/ buildCherryPickFiles(CherryPickRequest cherryPickRequest,
Set<CherryPickAssayPlate> forPlates)
{
MultiMap assayPlate2SourcePlateTypes = getSourcePlateTypesForEachAssayPlate(cherryPickRequest);
MultiMap result = MultiValueMap.decorate(new TreeMap<String,SortedSet<LabCherryPick>>(),
new Factory()
{
public Object create() { return new TreeSet<LabCherryPick>(PlateMappingCherryPickComparator.getInstance()); }
});
// HACK: transform set of CPAP into a set of IDs, for purpose of checking
// set membership; we can't rely upon CPAP.equals(), since we're comparing
// non-managed entities with managed entities, and therefore we do not have
// the guarantee of instance equality for entities with the same ID
Set<Serializable> forPlateIds = new HashSet<Serializable>( forPlates .size());
for (CherryPickAssayPlate cpap : forPlates) {
if (cpap.getEntityId() == null) {
throw new IllegalArgumentException("all members of 'forPlates' must already be persisted and have a database identifier");
}
forPlateIds.add(cpap.getEntityId());
}
for (LabCherryPick cherryPick : cherryPickRequest.getLabCherryPicks()) {
if (cherryPick.isAllocated()) {
CherryPickAssayPlate assayPlate = cherryPick.getAssayPlate();
if (forPlates == null || (assayPlate != null && forPlateIds.contains(assayPlate.getEntityId()))) {
Set<PlateType> sourcePlateTypes = (Set<PlateType>) assayPlate2SourcePlateTypes.get(assayPlate.getName());
String fileName = makeFilename(cherryPick, sourcePlateTypes.size());
result.put(fileName, cherryPick);
}
}
}
return result;
}
private String makeFilename(LabCherryPick cherryPick, int distinctSourcePlateTypes)
{
StringBuilder fileName = new StringBuilder(cherryPick.getAssayPlate().getName());
if (distinctSourcePlateTypes > 1) {
fileName.append(' ');
fileName.append(cherryPick.getSourceCopy().findPlate(cherryPick.getSourceWell().getPlateNumber()).getPlateType().toString());
}
int attempt = cherryPick.getAssayPlate().getAttemptOrdinal() + 1;
if (attempt > 0) {
fileName.append( " (Run").append(attempt).append(")");
}
fileName.append(PLATE_MAP_FILE_EXTENSION);
return fileName.toString();
}
private MultiMap getSourcePlateTypesForEachAssayPlate(CherryPickRequest cherryPickRequest)
{
MultiMap assayPlateName2PlateTypes = MultiValueMap.decorate(new HashMap(),
new Factory()
{
public Object create() { return new HashSet(); }
});
for (LabCherryPick cherryPick : cherryPickRequest.getLabCherryPicks()) {
if (cherryPick.isAllocated() && cherryPick.isMapped()) {
assayPlateName2PlateTypes.put(cherryPick.getAssayPlate().getName(),
cherryPick.getSourceCopy().findPlate(cherryPick.getSourceWell().getPlateNumber()).getPlateType());
}
}
return assayPlateName2PlateTypes;
}
private void writeCherryPickRow(PrintWriter out, LabCherryPick cherryPick)
{
out.print(cherryPick.getSourceWell().getPlateNumber());
out.print(cherryPick.getSourceCopy().getName());
out.print(cherryPick.getSourceWell().getWellName());
out.print(cherryPick.getSourceCopy().findPlate(cherryPick.getSourceWell().getPlateNumber()).getPlateType());
out.print(cherryPick.getAssayPlateWellName());
out.print(cherryPick.getAssayPlate().getAssayPlateType());
out.print(cherryPick.getCherryPickRequest().getRequestedBy().getFullNameFirstLast());
out.print(cherryPick.getCherryPickRequest().getScreen().getFacilityId());
out.print(cherryPick.getCherryPickRequest().getTransferVolumePerWellApproved().convert(VolumeUnit.MICROLITERS).getValue().setScale(2, RoundingMode.HALF_UP));
out.println();
}
private void writeHeadersRow(PrintWriter out)
{
for (String string : CSV_FILE_HEADERS) {
out.print(string);
}
out.println();
}
}