// $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.model.libraries;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.google.common.base.Function;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.log4j.Logger;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Sort;
import org.hibernate.annotations.SortType;
import org.hibernate.annotations.Type;
import org.joda.time.LocalDate;
import edu.harvard.med.screensaver.db.LibrariesDAO;
import edu.harvard.med.screensaver.model.AbstractEntityVisitor;
import edu.harvard.med.screensaver.model.AuditedAbstractEntity;
import edu.harvard.med.screensaver.model.DuplicateEntityException;
import edu.harvard.med.screensaver.model.activities.AdministrativeActivity;
import edu.harvard.med.screensaver.model.activities.AdministrativeActivityType;
import edu.harvard.med.screensaver.model.annotations.ToMany;
import edu.harvard.med.screensaver.model.annotations.ToOne;
import edu.harvard.med.screensaver.model.meta.Cardinality;
import edu.harvard.med.screensaver.model.meta.PropertyPath;
import edu.harvard.med.screensaver.model.meta.RelationshipPath;
import edu.harvard.med.screensaver.model.screenresults.AssayPlate;
import edu.harvard.med.screensaver.model.screens.ScreenType;
import edu.harvard.med.screensaver.model.users.AdministratorUser;
import edu.harvard.med.screensaver.model.users.ScreeningRoomUser;
import edu.harvard.med.screensaver.util.DevelopmentException;
/**
* A library represents a set of reagents and their layout into wells across
* multiple stock plates, and also includes the layout of control and other
* special purpose wells. The reagents comprising a given library are intended
* to be a cohesive set that arbitrarily groups the reagents by vendor, species
* targeted, cellular function, chemical similarity, or any combination thereof.
* Reagents may belong to multiple libraries.
* <ul>
* <li>Screensaver supports libraries for either RNAi and Small Molecule screens. RNAi library wells contain silencing
* reagents and small molecule library wells contain compounds.</li>
* <li>96-, 384-, and 1536-well {@link PlateSize plate sizes} are currently supported.</li>
* <li>A library must be defined for a set of plates that have a sequential plate numbers.</li>
* </ul>
* <p>
* A Library in Screensaver is the <i>definition</i> of a library, and does not imply that the library plates are
* physically present at the screening facility. The instances of a library being maintained at the screening facility
* are tracked by library {@link Copy copies}.
* <p>
* The domain model allows for a Library to be defined independently of its {@link Well wells} and its well
* {@link Reagent reagents}. In other words, the domain model permits any of the following states for a Library:
* <ul>
* <li>Library is defined with related Wells and Reagents. This is the normal state of a Library in Screensaver.
* <li>Library is defined with related Wells, but without Reagents ("library contents"). This allows for a library's
* contents to be unloaded and reloaded without deleting the Library definition itself, which is useful if
* updated/corrected well contents data becomes available.
* <li>Library is defined, but without related Wells, and thus without Reagents. It is recommended that Library's
* {@link Well wells} always be created at the same time a Library is created, even if the Wells are initially all
* {@link LibraryWellType#EMPTY empty}.
* </ul>
*
* @author <a mailto="john_sullivan@hms.harvard.edu">John Sullivan</a>
* @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a>
*/
@Entity
@org.hibernate.annotations.Proxy
public class Library extends AuditedAbstractEntity<Integer>
{
// static data
private static final Logger log = Logger.getLogger(Library.class);
private static final long serialVersionUID = 0L;
public static final RelationshipPath<Library> contentsVersions = RelationshipPath.from(Library.class).to("contentsVersions");
public static final RelationshipPath<Library> latestReleasedContentsVersion = RelationshipPath.from(Library.class).to("latestReleasedContentsVersion", Cardinality.TO_ONE);
public static final RelationshipPath<Library> wells = RelationshipPath.from(Library.class).to("wells");
public static final RelationshipPath<Library> copies = RelationshipPath.from(Library.class).to("copies");
public static final PropertyPath<Library> startPlate = RelationshipPath.from(Library.class).toProperty("startPlate");
public static final PropertyPath<Library> endPlate = RelationshipPath.from(Library.class).toProperty("endPlate");
public static final Function<Library,String> ToShortName = new Function<Library,String>() {
@Override
public String apply(Library l)
{
return l.getShortName();
}
};
// private instance data
private Integer _version;
private SortedSet<Well> _wells = Sets.newTreeSet();
private SortedSet<Copy> _copies = Sets.newTreeSet();
private String _libraryName;
private String _shortName;
private String _description;
private String _provider;
private ScreenType _screenType;
private LibraryType _libraryType;
private Solvent _solvent;
private boolean _isPool;
private Integer _startPlate;
private Integer _endPlate;
private LibraryScreeningStatus _screeningStatus;
private LocalDate _dateReceived;
private LocalDate _dateScreenable;
private PlateSize _plateSize;
private ScreeningRoomUser _owner;
private Integer _experimentalWellCount = new Integer(0);
private SortedSet<LibraryContentsVersion> _contentsVersions = Sets.newTreeSet();
private LibraryContentsVersion _latestReleasedContentsVersion;
// public constructor
/**
* @motivation for hibernate and proxy/concrete subclass constructors
*/
protected Library() {}
/**
*
* Construct a new, unitialized Library
* @motivation for new Library creation via user interface, where even required
* fields are allowed to be uninitialized, initially
* @param createdBy
*/
public Library(AdministratorUser createdBy)
{
super(createdBy);
}
/**
* Construct an initialized <code>Library</code> object.
*
* @param libraryName the library name
* @param shortName the short name
* @param screenType the screen type (RNAi or Small Molecule)
* @param libraryType the library type
* @param startPlate the start plate
* @param endPlate the end plate
*/
public Library(AdministratorUser createdBy,
String libraryName,
String shortName,
ScreenType screenType,
LibraryType libraryType,
Integer startPlate,
Integer endPlate,
PlateSize plateSize)
{
super(createdBy);
_libraryName = libraryName;
_shortName = shortName;
_screenType = screenType;
_solvent = Solvent.getDefaultSolventType(_screenType);
_libraryType = libraryType;
_startPlate = startPlate;
_endPlate = endPlate;
_plateSize = plateSize;
setScreeningStatus(LibraryScreeningStatus.ALLOWED);
}
@Override
public Object acceptVisitor(AbstractEntityVisitor visitor)
{
return visitor.visit(this);
}
/**
* Get the id for the screening library.
*
* @return the id for the screening library
*/
@Id
@org.hibernate.annotations.GenericGenerator(name = "library_id_seq", strategy = "sequence", parameters = { @Parameter(name = "sequence", value = "library_id_seq") })
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "library_id_seq")
public Integer getLibraryId()
{
return getEntityId();
}
@ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL })
@JoinTable(name="libraryUpdateActivity",
joinColumns=@JoinColumn(name="libraryId", nullable=false, updatable=false),
inverseJoinColumns=@JoinColumn(name="updateActivityId", nullable=false, updatable=false))
@Sort(type=SortType.NATURAL)
@ToMany(singularPropertyName="updateActivity", hasNonconventionalMutation=true /* model testing framework doesn't understand this is a containment relationship, and so requires addUpdateActivity() method*/)
@Override
public SortedSet<AdministrativeActivity> getUpdateActivities()
{
return _updateActivities;
}
/**
* Get the set of wells.
*
* @return the wells
*/
@OneToMany(mappedBy = "library", cascade = { CascadeType.ALL })
@Sort(type = SortType.NATURAL)
public SortedSet<Well> getWells()
{
return _wells;
}
/**
* Get the number of wells.
*
* @return the number of wells
* @motivation {@link #getWells} forces loading of all wells, just to get the
* size; Hibernate can optimize a collection size request, if we
* get directly from an underyling extra-lazy persistent
* collection.
*/
@Transient
public int getNumWells()
{
return _wells.size();
}
/**
* Create and return a new well for the library.
*
* @param wellKey the well key for the new well
* @param wellType the well type for the new well
* @return the new well
*/
public Well createWell(WellKey wellKey, LibraryWellType wellType)
{
Well well = new Well(this, wellKey, wellType);
if (!_wells.add(well)) {
throw new DuplicateEntityException(this, well);
}
return well;
}
/**
* Get the copies.
*
* @return the copies
*/
@OneToMany(mappedBy = "library", cascade = { CascadeType.ALL })
@Sort(type = SortType.NATURAL)
public SortedSet<Copy> getCopies()
{
return _copies;
}
/**
* Get the copy with the given copy name
*
* @param copyName the copy name of the copy to get
* @return the copy with the given copy name
*/
@Transient
public Copy getCopy(final String copyName)
{
return (Copy) CollectionUtils.find(_copies, new Predicate() {
public boolean evaluate(Object e)
{
return ((Copy) e).getName().equals(copyName);
};
});
}
/**
* Create a new copy for the library.
*
* @param usageType the copy usage type
* @param name the copy name
*/
public Copy createCopy(AdministratorUser createdBy, CopyUsageType usageType, String name)
{
Copy copy = new Copy(createdBy, this, usageType, name);
addCopy(copy);
return copy;
}
public void addCopy(Copy copy)
{
if (!_copies.add(copy)) {
throw new DuplicateEntityException(this, copy);
}
}
/**
* Get the library name.
*
* @return the library name
*/
@Column(unique = true, nullable = false)
@org.hibernate.annotations.Type(type = "text")
public String getLibraryName()
{
return _libraryName;
}
/**
* Set the library name.
*
* @param libraryName the new library name
*/
public void setLibraryName(String libraryName)
{
_libraryName = libraryName;
}
/**
* Get the short name.
*
* @return the short name
*/
@Column(unique = true, nullable = false)
@org.hibernate.annotations.Type(type = "text")
public String getShortName()
{
return _shortName;
}
/**
* Set the short name.
*
* @param shortName the new short name
*/
public void setShortName(String shortName)
{
_shortName = shortName;
}
/**
* Get the description.
*
* @return the description
*/
@org.hibernate.annotations.Type(type = "text")
public String getDescription()
{
return _description;
}
/**
* Set the description.
*
* @param description the new description
*/
public void setDescription(String description)
{
_description = description;
}
/**
* Get the provider of the library. The provider may be a commercial vendor, an academic lab, etc. Note that a library
* may be comprised of reagents from multiple {@link Reagent#getVendorId() vendors}, and these vendor(s) are not
* necessarily the same as the library's provider.
*/
@org.hibernate.annotations.Type(type = "text")
public String getProvider()
{
return _provider;
}
public void setProvider(String provider)
{
_provider = provider;
}
/**
* Get the screen type.
*
* @return the screen type
*/
@Column(nullable = false)
@org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.screens.ScreenType$UserType")
public ScreenType getScreenType()
{
return _screenType;
}
/**
* Set the screen type.
*
* @param screenType the new screen type
*/
public void setScreenType(ScreenType screenType)
{
_screenType = screenType;
}
/**
* Get the library type.
*
* @return the library type
*/
@Column(nullable = false)
@org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.libraries.LibraryType$UserType")
public LibraryType getLibraryType()
{
return _libraryType;
}
/**
* Set the library type.
*
* @param libraryType the new library type
*/
public void setLibraryType(LibraryType libraryType)
{
_libraryType = libraryType;
}
@Column(nullable = false)
@org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.libraries.Solvent$UserType")
public Solvent getSolvent()
{
return _solvent;
}
public void setSolvent(Solvent solvent)
{
_solvent = solvent;
}
/**
* Determines whether this library's well contains pools of reagents. Intended
* for use with RNAi libraries (in particular, Dharmacon siGENOME libraries),
* but can be commandeered for any applicable library.
*
* @return true if this library's wells contains pools of reagents, otherwise
* may return false or null.
*/
@Column(name="isPool", nullable=false)
public boolean isPool()
{
return _isPool;
}
public void setPool(boolean isPool)
{
_isPool = isPool;
}
@Transient
public Class<? extends Reagent> getReagentType()
{
if (_screenType == ScreenType.SMALL_MOLECULE) {
if (_libraryType == LibraryType.NATURAL_PRODUCTS) {
return NaturalProductReagent.class;
}
return SmallMoleculeReagent.class;
}
else if (_screenType == ScreenType.RNAI) {
return SilencingReagent.class;
}
throw new DevelopmentException("unhandled screen/library type");
}
/**
* Get the start plate.
*
* @return the start plate
*/
@Column(unique = true, nullable = false)
public Integer getStartPlate()
{
return _startPlate;
}
/**
* Set the start plate.
*
* @param startPlate the new start plate
*/
public void setStartPlate(Integer startPlate)
{
_startPlate = startPlate;
}
/**
* Get the end plate.
*
* @return the end plate
*/
@Column(unique = true, nullable = false)
public Integer getEndPlate()
{
return _endPlate;
}
/**
* Set the end plate.
*
* @param endPlate the new end plate
*/
public void setEndPlate(Integer endPlate)
{
_endPlate = endPlate;
}
/**
* Return true iff this library contains the specified plate.
*
* @param plateNumber
* @return true iff this library contains the specified plate
*/
public boolean containsPlate(Integer plateNumber)
{
return plateNumber != null && plateNumber >= getStartPlate() &&
plateNumber <= getEndPlate();
}
/**
* Get the screening status of this library, indicating whether this library
* is available for screening.
*
* @return the screening status
*/
@org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.libraries.LibraryScreeningStatus$UserType")
@Column(nullable=false)
public LibraryScreeningStatus getScreeningStatus()
{
return _screeningStatus;
}
/**
* Set the screening status
*
* @param screeningStatus the new screening status
*/
public void setScreeningStatus(LibraryScreeningStatus screeningStatus)
{
_screeningStatus = screeningStatus;
}
/**
* Get the date received.
*
* @return the date received
*/
@Type(type = "edu.harvard.med.screensaver.db.usertypes.LocalDateType")
public LocalDate getDateReceived()
{
return _dateReceived;
}
/**
* Set the date received.
*
* @param dateReceived the new date received
*/
public void setDateReceived(LocalDate dateReceived)
{
_dateReceived = dateReceived;
}
/**
* Get the date screenable.
*
* @return the date screenable
*/
@Type(type = "edu.harvard.med.screensaver.db.usertypes.LocalDateType")
public LocalDate getDateScreenable()
{
return _dateScreenable;
}
/**
* Set the date screenable.
*
* @param dateScreenable the new date screenable
*/
public void setDateScreenable(LocalDate dateScreenable)
{
_dateScreenable = dateScreenable;
}
// private instance methods
/**
* Set the id for the screening library.
*
* @param libraryId the new id for the screening library
* @motivation for hibernate
*/
private void setLibraryId(Integer libraryId)
{
setEntityId(libraryId);
}
/**
* Get the version for the screening library.
*
* @return the version for the screening library
* @motivation for hibernate
*/
@Version
@Column(nullable = false)
private Integer getVersion()
{
return _version;
}
/**
* Set the version for the screening library.
*
* @param version the new version for the screening library
* @motivation for hibernate
*/
private void setVersion(Integer version)
{
_version = version;
}
/**
* Set the set of wells.
*
* @param wells the new set of wells
* @motivation for hibernate
*/
private void setWells(SortedSet<Well> wells)
{
_wells = wells;
}
/**
* Set the set of copies.
*
* @param copies the new set of copies
* @motivation for hibernate
*/
private void setCopies(SortedSet<Copy> copies)
{
_copies = copies;
}
@Column(nullable=false/*, updatable=false*/)
//@org.hibernate.annotations.Immutable
@org.hibernate.annotations.Type(type="edu.harvard.med.screensaver.model.libraries.PlateSize$UserType")
public PlateSize getPlateSize()
{
return _plateSize;
}
/**
* Note: if plateSize is changed, it is the responsibility of the caller to
* also add/remove wells, as necessary, to match the new plate size.
*
* @param plateSize the new PlateSize
*/
public void setPlateSize(PlateSize plateSize)
{
// if (!isHibernateCaller() && getEntityId() != null && plateSize != _plateSize) {
// throw new DataModelViolationException("cannot change plate size after library is created");
// }
_plateSize = plateSize;
}
@edu.harvard.med.screensaver.model.annotations.Column(hasNonconventionalSetterMethod=true)
public Integer getExperimentalWellCount()
{
return _experimentalWellCount;
}
private void setExperimentalWellCount(Integer experimentalWellCount)
{
this._experimentalWellCount = experimentalWellCount;
}
void incExperimentalWellCount()
{
_experimentalWellCount = _experimentalWellCount + 1;
}
void decExperimentalWellCount()
{
assert _experimentalWellCount > 0;
_experimentalWellCount = Math.max(0, _experimentalWellCount - 1);
}
@OneToMany(mappedBy = "library", cascade = { CascadeType.ALL }, orphanRemoval = true)
@org.hibernate.annotations.Sort(type=org.hibernate.annotations.SortType.NATURAL)
public SortedSet<LibraryContentsVersion> getContentsVersions()
{
return _contentsVersions;
}
private void setContentsVersions(SortedSet<LibraryContentsVersion> contentsVersions)
{
_contentsVersions = contentsVersions;
}
/**
* Get the most recently released {@link LibraryContentsVersion}, which
* represents the latest contents version that is available for viewing by
* {@link ScreeningRoomUser}s. Note that there may exist a newer
* contents versions that has not yet been released for viewing, and so is
* only available to {@link AdministratorUser}s.
*/
@OneToOne
@JoinColumn(name="latest_released_contents_version_id")
@ToOne(hasNonconventionalSetterMethod=true) /* the released contents versions must be one of the library's contents versions */
public LibraryContentsVersion getLatestReleasedContentsVersion()
{
return _latestReleasedContentsVersion;
}
/*package*/ void setLatestReleasedContentsVersion(LibraryContentsVersion latestReleasedContentsVersion)
{
_latestReleasedContentsVersion = latestReleasedContentsVersion;
}
/**
* Get the most recently created {@link LibraryContentsVersion}, which may be
* newer than the latest <i>released</i> contents version, and so may only be
* available to {@link AdministratorUser}s.
*/
@Transient
public LibraryContentsVersion getLatestContentsVersion()
{
if (_contentsVersions.isEmpty()) {
return null;
}
return _contentsVersions.last();
}
/**
* Create a new {@link LibraryContentsVersion}. This contents version will not
* be available for viewing by {@link ScreeningRoomUser}s until its has been
* released by calling
* {@link #setLatestReleasedContentsVersion(LibraryContentsVersion)}.
*
* @return the new {@link LibraryContentsVersion}
*/
public LibraryContentsVersion createContentsVersion(AdministratorUser recordedBy)
{
AdministrativeActivity loadingAdminActivity = new AdministrativeActivity(recordedBy, new LocalDate(), AdministrativeActivityType.LIBRARY_CONTENTS_LOADING);
LibraryContentsVersion libraryContentsVersion =
new LibraryContentsVersion(this,
_contentsVersions.isEmpty() ? LibraryContentsVersion.FIRST_VERSION_NUMBER : _contentsVersions.last().getVersionNumber() + 1,
loadingAdminActivity);
_contentsVersions.add(libraryContentsVersion);
return libraryContentsVersion;
}
//Set the FetchType to EAGER otherwise when browsing libraries: org.hibernate.LazyInitializationException: could not initialize proxy - no Session
@ManyToOne(fetch=FetchType.EAGER, cascade={ CascadeType.PERSIST, CascadeType.MERGE })
@JoinColumn(name="ownerScreenerId", nullable=true)
@org.hibernate.annotations.ForeignKey(name="fk_library_to_owner")
// @org.hibernate.annotations.LazyToOne(value=org.hibernate.annotations.LazyToOneOption.PROXY)
@org.hibernate.annotations.Cascade(value={
org.hibernate.annotations.CascadeType.SAVE_UPDATE
})
public ScreeningRoomUser getOwner()
{
return _owner;
}
public void setOwner(ScreeningRoomUser owner)
{
_owner = owner;
}
@Transient
public SortedSet<LibraryPlate> getLibraryPlates()
{
SetMultimap<Integer,AssayPlate> index = HashMultimap.create();
for (Copy copy : getCopies()) {
for (Map.Entry<Integer,Plate> entry : copy.getPlates().entrySet()) {
index.putAll(entry.getKey(), entry.getValue().getAssayPlates());
}
}
SortedSet<LibraryPlate> libraryPlates = Sets.newTreeSet();
Set<AssayPlate> assayPlates;
for (int p = getStartPlate(); p <= getEndPlate(); ++p) {
if (index.containsKey(p)) {
assayPlates = index.get(p);
}
else {
assayPlates = Collections.emptySet();
}
libraryPlates.add(new LibraryPlate(p,
this,
assayPlates));
}
return libraryPlates;
}
// [#3439] Old well values are not nulled out before reloading new library content versions
// TODO: the well contents are not held back for the LCV release, it appears, since the LCV release only affects the latest released reagent -sde4
public void resetContents() {
for(Well well: getWells())
{
well.resetLibraryContents();
}
}
}