// $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.screenresults; 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.ManyToOne; import javax.persistence.Table; import javax.persistence.Transient; import javax.persistence.UniqueConstraint; import org.apache.log4j.Logger; import org.hibernate.annotations.Immutable; import org.hibernate.annotations.Index; import edu.harvard.med.screensaver.model.AbstractEntity; import edu.harvard.med.screensaver.model.AbstractEntityVisitor; import edu.harvard.med.screensaver.model.DataModelViolationException; import edu.harvard.med.screensaver.model.libraries.LibraryWellType; import edu.harvard.med.screensaver.model.libraries.Well; import edu.harvard.med.screensaver.model.meta.Cardinality; import edu.harvard.med.screensaver.model.meta.RelationshipPath; import edu.harvard.med.screensaver.util.DevelopmentException; /** * A <code>ResultValue</code> holds the value of a screen result data point for * a given {@link DataColumn}, and {@link AssayWell}. For text-based * DataColumns, the value is stored canonically as a String. For numeric * DataColumns, the value is stored canonically as a double, allowing for * efficient sorting and filtering of numeric values in the database. Note that * the parent {@link DataColumn} contains an {@link DataColumn#isNumeric()} property * that indicates whether its member ResultValues are numeric (the isNumeric * flag is not stored with each ResultValue for space efficiency). * * @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a> * @author <a mailto="john_sullivan@hms.harvard.edu">John Sullivan</a> */ @Entity @Immutable @org.hibernate.annotations.Proxy @edu.harvard.med.screensaver.model.annotations.ContainedEntity(containingEntityClass=DataColumn.class) @Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "dataColumnId", "well_id" }) }) @org.hibernate.annotations.Table(appliesTo = "result_value", indexes = { @Index(name = "result_value_data_column_and_value_index", columnNames = { "dataColumnId", "value" }), @Index(name = "result_value_data_column_and_numeric_value_index", columnNames = { "dataColumnId", "numericValue" }) }) public class ResultValue extends AbstractEntity<Integer> { private static final long serialVersionUID = -4066041317098744417L; private static final Logger log = Logger.getLogger(ResultValue.class); public static final RelationshipPath<ResultValue> DataColumn = RelationshipPath.from(ResultValue.class).to("dataColumn", Cardinality.TO_ONE); public static final RelationshipPath<ResultValue> Well = RelationshipPath.from(ResultValue.class).to("well", Cardinality.TO_ONE); private Well _well; private DataColumn _dataColumn; private String _value; private Double _numericValue; private AssayWellControlType _assayWellControlType; /** * Note that we maintain an "exclude" flag on a per-ResultValue basis. It is * up to the application code and/or user interface to manage excluding the * full set of ResultValues associated with a stock plate well (row) or with a * data column. But we do need to allow any arbitrary set of * ResultValues to be excluded. */ private boolean _isExclude; private boolean _isPositive; private PartitionedValue _partitionedPositiveValue; private Boolean _booleanPositiveValue; private ConfirmedPositiveValue _confirmedPositiveValue; /** * Constructs a <code>ResultValue</code>. * Construct an initialized <code>ResultValue</code>. Intended only for use by * this class's constructors and {@link DataColumn}. * * @param dataColumn the parent DataColumn * @param assayWell the Assaywell of this ResultValue * @param value * @param numericValue * @param partitionPositiveIndicatorValue * @param booleanPositiveIndicatorValue * @param exclude whether this ResultValue is to be (or was) ignored when performing analysis for the determination of * positives */ ResultValue(DataColumn dataColumn, AssayWell assayWell, String value, Double numericValue, PartitionedValue partitionPositiveIndicatorValue, Boolean booleanPositiveIndicatorValue, ConfirmedPositiveValue confirmedPositiveIndicatorValue, boolean exclude) { if (dataColumn == null) { throw new DataModelViolationException("dataColumn is required for ResultValue"); } if (assayWell == null) { throw new DataModelViolationException("assay well is required for ResultValue"); } _dataColumn = dataColumn; _well = assayWell.getLibraryWell(); // TODO: remove // TODO: HACK: removing this update as it causes memory/performance // problems when loading ScreenResults; fortunately, when ScreenResult is // read in from database from a new Hibernate session, the in-memory // associations will be correct; these in-memory associations will only be // missing within the Hibernate session that was used to import the // ScreenResult // _well.getResultValues().put(dataColumn, this); setAssayWellControlType(assayWell.getAssayWellControlType()); // TODO: remove setExclude(exclude); switch (dataColumn.getDataType()) { case NUMERIC: _numericValue = numericValue; break; case POSITIVE_INDICATOR_BOOLEAN: setBooleanPositiveValue(booleanPositiveIndicatorValue); break; case POSITIVE_INDICATOR_PARTITION: setPartitionedPositiveValue(partitionPositiveIndicatorValue); break; case CONFIRMED_POSITIVE_INDICATOR: setConfirmedPositiveValue(confirmedPositiveIndicatorValue); break; case TEXT: setValue(value); break; default: throw new DevelopmentException("unhandled data type"); } } private void setConfirmedPositiveValue(ConfirmedPositiveValue value) { if (!isHibernateCaller()) { if (value == null) { value = ConfirmedPositiveValue.NOT_TESTED; } if (value == ConfirmedPositiveValue.CONFIRMED_POSITIVE && isPositiveCandidate()) { setPositive(true); } setValue(value.toStorageValue()); } _confirmedPositiveValue = value; } /** * Constructs a numeric ResultValue object * Returns the value of this <code>ResultValue</code> as an appropriately * typed object, depending upon the {@link DataColumn#getDataType() data column's data type}, as follows: * <ul> * <li>Well type is non-data-producer: returns <code>null</code> * <li>Not Derived (Raw): returns Double * <li>Not an Activity Indicator: returns String * <li>DataType.BOOLEAN: returns Boolean * <li>DataType.PARTITION: returns String (PartitionedValue.getDisplayValue()) * </ul> * * @return a Boolean, Double, or String * @motivation to preserve typed data in exported Workbooks (rather than treat * all result values as text strings) */ @Transient public Object getTypedValue() { if (isNull()) { return null; } switch (getDataColumn().getDataType()) { case NUMERIC: return getNumericValue(); case POSITIVE_INDICATOR_BOOLEAN: return getBooleanPositiveValue(); case POSITIVE_INDICATOR_PARTITION: return getPartitionedPositiveValue(); case CONFIRMED_POSITIVE_INDICATOR: return getConfirmedPositiveValue(); default: return getValue(); } } @Column(name = "value", updatable = false, insertable = false) @org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.screenresults.ConfirmedPositiveValueUserType") public ConfirmedPositiveValue getConfirmedPositiveValue() { return _confirmedPositiveValue; } @Column(name = "value", updatable = false, insertable = false) @org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.screenresults.PartitionedValueUserType") public PartitionedValue getPartitionedPositiveValue() { return _partitionedPositiveValue; } private void setPartitionedPositiveValue(PartitionedValue value) { if (!isHibernateCaller()) { if (value == null) { value = PartitionedValue.NOT_POSITIVE; } if (value != PartitionedValue.NOT_POSITIVE && isPositiveCandidate()) { setPositive(true); } setValue(value.toStorageValue()); } _partitionedPositiveValue = value; } @Column(name = "value", updatable = false, insertable = false) @org.hibernate.annotations.Type(type = "edu.harvard.med.screensaver.model.screenresults.BooleanPositiveValueUserType") public Boolean getBooleanPositiveValue() { return _booleanPositiveValue; } private void setBooleanPositiveValue(Boolean value) { if (!isHibernateCaller()) { if (value == null) { value = Boolean.FALSE; } if (value.equals(Boolean.TRUE) && isPositiveCandidate()) { setPositive(true); } setValue(value.toString()); } _booleanPositiveValue = value; } @Override public Object acceptVisitor(AbstractEntityVisitor visitor) { return visitor.visit(this); } @Id @org.hibernate.annotations.GenericGenerator( name="result_value_id_seq", strategy="seqhilo", parameters = { @org.hibernate.annotations.Parameter(name="sequence", value="result_value_id_seq"), @org.hibernate.annotations.Parameter(name="max_lo", value="384") } ) @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="result_value_id_seq") public Integer getResultValueId() { return getEntityId(); } /** * Get the data column. * @return the data column */ @ManyToOne(optional = false) @JoinColumn(name = "dataColumnId") @org.hibernate.annotations.ForeignKey(name="fk_result_value_to_data_column") @org.hibernate.annotations.LazyToOne(value = org.hibernate.annotations.LazyToOneOption.PROXY) public DataColumn getDataColumn() { return _dataColumn; } /** * Get the well. * @return the well */ @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="well_id", nullable=false, updatable=false) @org.hibernate.annotations.ForeignKey(name="fk_result_value_to_well") @org.hibernate.annotations.LazyToOne(value=org.hibernate.annotations.LazyToOneOption.PROXY) public Well getWell() { return _well; } /** * Get the string value of this <code>ResultValue</code>. * * @return a {@link java.lang.String} representing the string value of this * <code>ResultValue</code>; may return null. */ @org.hibernate.annotations.Type(type="text") @edu.harvard.med.screensaver.model.annotations.Column(hasNonconventionalSetterMethod=true /* automated model tests can only test one type of result value, numeric or non-numeric, and since numeric is the more common choice that's the one we test automatically */) public String getValue() { return _value; } /** * Return true whenever this result value has a null value. * * @return true whenever this result value has a null value * @motivation convenience, to avoid having to call each of {@link #getValue()}, {@link #getNumericValue()}, * {@link #getBooleanPositiveValue()}, and {@link #getPartitionedPositiveValue()} to determine * if ResultValue is null */ @Transient public boolean isNull() { return _value == null && _numericValue == null && _partitionedPositiveValue == null && _booleanPositiveValue == null; } /** * Get the numeric value of this <code>ResultValue</code>. * * @return a {@link java.lang.Double} representing the numeric value of this * <code>ResultValue</code>; may return null. */ public Double getNumericValue() { return _numericValue; } /** * @deprecated use {@link AssayWell#getAssayWellControlType}; this will be removed in the future */ @Deprecated @Column(nullable=true) @org.hibernate.annotations.Type(type="edu.harvard.med.screensaver.model.screenresults.AssayWellControlType$UserType") @edu.harvard.med.screensaver.model.annotations.Column(hasNonconventionalSetterMethod=true /* set via parent AssayWel.assayWellControlType */) public AssayWellControlType getAssayWellControlType() { return _assayWellControlType; } /** * Get whether this <code>ResultValue</code> is to be excluded in any * subsequent analyses. * * @return <code>true</code> iff this <code>ResultValue</code> is to be * excluded in any subsequent analysis */ @Column(nullable=false, name="isExclude") public boolean isExclude() { return _isExclude; } /** * Get whether this result value indicates a positive. Returns false if the {@link #getDataColumn() DataColumn} is not * a positive indicator. <i>Note: this flag may not agree with the screener-provided value (true/false, or S/M/W), as * it will not be set for screener-indicated positives that are not in experimental wells or that have been * {@link #isExclude() excluded}. * * @return true if this result value is a positive indicator */ @Column(nullable=false, name="isPositive") public boolean isPositive() { return _isPositive; } /** * Return true iff the assay well type is a control. * * @return true iff the assay well type is a control */ @Transient public boolean isControlWell() { return getWell().getLibraryWellType() == LibraryWellType.LIBRARY_CONTROL || getAssayWellControlType() != null; } @Transient public boolean isEdgeWell() { return getWell().isEdgeWell(); } /** * Return true iff the assay well type is data producing. * * @return true iff the assay well type is data producing */ @Transient public boolean isDataProducerWell() { return getWell().getLibraryWellType() == LibraryWellType.EXPERIMENTAL || isControlWell(); } /** * Set whether this result value is a positive. Intended only for use by * hibernate and {@link DataColumn}. * * @param isPositive true iff this result value is a positive * @motivation for hibernate and DataColumn */ void setPositive(boolean isPositive) { _isPositive = isPositive; } /** * Constructs an uninitialized ResultValue object. * * @motivation for hibernate */ private ResultValue() {} /** * Set the id for the result value. * @param resultValueId the new id for the result value * @motivation for hibernate */ private void setResultValueId(Integer resultValueId) { setEntityId(resultValueId); } /** * Set the well. * * @param well the new well * @motivation for hibernate */ private void setWell(Well well) { _well = well; } /** * Set the data column. * * @param dataColumn the new data column * @motivation for hibernate */ private void setDataColumn(DataColumn dataColumn) { _dataColumn = dataColumn; } /** * Set the actual value of this result value. * * @param value the new value of this result value * @motivation for hibernate */ private void setValue(String value) { _value = value; } /** * Set the numerical value of this result value * * @param value the new numerical value * @motivation for hibernate */ private void setNumericValue(Double value) { _numericValue = value; } /** * Set whether the screener has deemed that this <code>ResultValue</code> * should be excluded in any subsequent analyses. * * @param exclude set to <code>true</code> iff this <code>ResultValue</code> * is to be excluded in any subsequent analysis * @motivation for hibernate */ private void setExclude(boolean exclude) { _isExclude = exclude; } /** * Set the assay well's type. <i>Note: This is implemented as a denormalized * attribute. If you call this method, you must also call it for every * ResultValue that has the same Well (within the same parent screen result).</i> * Technically, we should have an AssayWell entity, which groups all the * ResultValues for a given stock plate well (within the parent screen * result). But it's creates a lot of new bidirectional relationships! * * @param assayWellControlType the new type of the assay well * @motivation for hibernate */ private void setAssayWellControlType(AssayWellControlType assayWellControlType) { _assayWellControlType = assayWellControlType; } @Transient private boolean isPositiveCandidate() { return !isExclude() && getWell().getLibraryWellType() == LibraryWellType.EXPERIMENTAL; } }