diff --git a/gemma-core/src/main/java/ubic/gemma/model/common/quantitationtype/QuantitationType.java b/gemma-core/src/main/java/ubic/gemma/model/common/quantitationtype/QuantitationType.java index e8a58212dd..ab1b243ed5 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/common/quantitationtype/QuantitationType.java +++ b/gemma-core/src/main/java/ubic/gemma/model/common/quantitationtype/QuantitationType.java @@ -21,6 +21,7 @@ import ubic.gemma.model.common.AbstractDescribable; import java.io.Serializable; +import java.util.Objects; public class QuantitationType extends AbstractDescribable implements Serializable { @@ -214,6 +215,10 @@ public boolean equals( Object object ) { } final QuantitationType that = ( QuantitationType ) object; + if ( that.getId() != null && this.getId() != null ) { + return Objects.equals( that.getId(), this.getId() ); + } + if ( that.getName() != null && this.getName() != null && !this.getName().equals( that.getName() ) ) { return false; } diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/BulkExpressionDataVector.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/BulkExpressionDataVector.java index 488e40f9bc..6bde43e4a3 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/BulkExpressionDataVector.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/BulkExpressionDataVector.java @@ -34,10 +34,7 @@ public boolean equals( Object object ) { @Override public int hashCode() { - if ( getId() != null ) { - return Objects.hashCode( getId() ); - } - return Objects.hash( super.hashCode(), Objects.hashCode( bioAssayDimension ) ); + return Objects.hash( super.hashCode(), bioAssayDimension ); } @Override diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DataVector.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DataVector.java index 5e9c2fa72d..891fa54ecf 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DataVector.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DataVector.java @@ -48,10 +48,9 @@ public abstract class DataVector implements Identifiable, Serializable { */ @Override public int hashCode() { - if ( id != null ) { - return Objects.hashCode( id ); - } - return Objects.hash( expressionExperiment, quantitationType, Arrays.hashCode( data ) ); + // also, we cannot hash the ID because it is assigned on creation + // hashing the data is wasteful because subclasses will have a design element to distinguish distinct vectors + return Objects.hash( expressionExperiment, quantitationType ); } /** diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DesignElementDataVector.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DesignElementDataVector.java index a89b47af30..3442bfca0b 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DesignElementDataVector.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/DesignElementDataVector.java @@ -37,9 +37,6 @@ public class DesignElementDataVector extends DataVector { @Override public int hashCode() { - if ( getId() != null ) { - return Objects.hash( getId() ); - } return Objects.hash( super.hashCode(), designElement ); } diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.java index 73b85f10c3..6487e0f3dc 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.java @@ -2,19 +2,18 @@ import lombok.Getter; import lombok.Setter; +import org.springframework.util.Assert; import ubic.gemma.core.util.ListUtils; import ubic.gemma.model.common.Identifiable; import ubic.gemma.model.expression.bioAssay.BioAssay; +import ubic.gemma.persistence.hibernate.ByteArrayType; import ubic.gemma.persistence.hibernate.CompressedStringListType; -import ubic.gemma.persistence.hibernate.IntArrayType; import javax.annotation.Nullable; import javax.persistence.Transient; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; +import static java.util.Collections.unmodifiableList; import static ubic.gemma.core.util.ListUtils.getSparseRangeArrayElement; @Getter @@ -30,7 +29,7 @@ public class SingleCellDimension implements Identifiable { *

* This is stored as a compressed, gzipped blob in the database. See {@link CompressedStringListType} for more details. */ - private List cellIds; + private List cellIds = new ArrayList<>(); /** * An internal collection for mapping cell IDs to their position in {@link #cellIds}. @@ -44,25 +43,33 @@ public class SingleCellDimension implements Identifiable { *

* This should always be equal to the size of {@link #cellIds}. */ - private Integer numberOfCells; + private int numberOfCells = 0; /** - * Cell types, or null if unknown. + * Cell types assignment to individual cells from the {@link #cellTypeLabels} collections. + *

+ * If supplied, its size must be equal to that of {@link #cellIds}. + */ + @Nullable + private int[] cellTypes; + + /** + * Cell type labels, or null if unknown. *

* Those are user-supplied cell type identifiers. Its size must be equal to that of {@link #cellIds}. *

* This is stored as a compressed, gzipped blob in the database. See {@link CompressedStringListType} for more details. */ @Nullable - private List cellTypes; + private List cellTypeLabels; /** - * Number of cell types. + * Number of distinct cell types. *

* This must always be equal to number of distinct elements of {@link #cellTypes}. */ @Nullable - private Integer numberOfCellTypes; + private Integer numberOfCellTypeLabels; /** * List of bioassays that each cell belongs to. @@ -70,16 +77,20 @@ public class SingleCellDimension implements Identifiable { * The {@link BioAssay} {@code bioAssays[i]} applies to all the cells in the interval {@code [bioAssaysOffset[i], bioAssaysOffset[i+1][}. * To find the bioassay type of a given cell, use {@link #getBioAssay(int)}. */ - private List bioAssays; + private List bioAssays = new ArrayList<>(); /** * Offsets of the bioassays. *

* This always contain {@code bioAssays.size()} elements. *

- * This is stored in the database using {@link IntArrayType}. + * This is stored in the database using {@link ByteArrayType}. */ - private int[] bioAssaysOffset; + private int[] bioAssaysOffset = new int[0]; + + public List getCellIds() { + return unmodifiableList( cellIds ); + } public void setCellIds( List cellIds ) { this.cellIds = cellIds; @@ -98,6 +109,23 @@ public BioAssay getBioAssay( int index ) { * Obtain the {@link BioAssay} for a given cell ID. */ public BioAssay getBioAssayByCellId( String cellId ) { + return getBioAssay( getCellIndex( cellId ) ); + } + + public String getCellTypeLabel( int index ) { + Assert.notNull( cellTypes, "No cell types have been assigned." ); + Assert.notNull( cellTypeLabels, "No cell labels exist." ); + return cellTypeLabels.get( cellTypes[index] ); + } + + /** + * Obtain a cell type label by cell ID. + */ + public String getCellTypeLabelByCellId( String cellId ) { + return getCellTypeLabel( getCellIndex( cellId ) ); + } + + private int getCellIndex( String cellId ) { if ( cellIdToIndex == null ) { cellIdToIndex = ListUtils.indexOfElements( cellIds ); } @@ -105,7 +133,7 @@ public BioAssay getBioAssayByCellId( String cellId ) { if ( index == null ) { throw new IllegalArgumentException( "Cell ID not found: " + cellId ); } - return getBioAssay( index ); + return index; } @Override @@ -114,7 +142,7 @@ public int hashCode() { return Objects.hash( id ); } // no need to hash numberOfCells, it's derived from cellIds's size - return Objects.hash( cellIds, cellTypes, cellTypes, bioAssays, Arrays.hashCode( bioAssaysOffset ) ); + return Objects.hash( cellIds, Arrays.hashCode( cellTypes ), cellTypeLabels, bioAssays, Arrays.hashCode( bioAssaysOffset ) ); } @Override @@ -129,8 +157,14 @@ public boolean equals( Object obj ) { } if ( id != null && ( ( SingleCellDimension ) obj ).id != null ) return id.equals( ( ( SingleCellDimension ) obj ).id ); - return Objects.equals( cellTypes, scd.cellTypes ) + return Objects.equals( cellTypeLabels, scd.cellTypeLabels ) && Objects.equals( bioAssays, scd.bioAssays ) + && Arrays.equals( cellTypes, scd.cellTypes ) && Objects.equals( cellIds, scd.cellIds ); // this is the most expensive to compare } + + @Override + public String toString() { + return String.format( "SingleCellDimension %s", id != null ? "Id=" + id : "" ); + } } diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.java index 15cb8a83b4..e15a59d465 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.java @@ -2,7 +2,10 @@ import lombok.Getter; import lombok.Setter; -import ubic.gemma.persistence.hibernate.IntArrayType; +import ubic.gemma.persistence.hibernate.ByteArrayType; + +import java.util.Arrays; +import java.util.Objects; /** * An expression data vector that contains data at the resolution of a single cell. @@ -26,7 +29,21 @@ public class SingleCellExpressionDataVector extends DesignElementDataVector { /** * Positions of the non-zero data in the {@link #getData()} vector. *

- * This is mapped in the database using {@link IntArrayType}. + * This is mapped in the database using {@link ByteArrayType}. */ private int[] dataIndices; + + @Override + public boolean equals( Object object ) { + if ( this == object ) { + return true; + } + if ( !( object instanceof SingleCellExpressionDataVector ) ) { + return false; + } + SingleCellExpressionDataVector other = ( SingleCellExpressionDataVector ) object; + return super.equals( object ) + && Objects.equals( singleCellDimension, other.singleCellDimension ) + && Arrays.equals( dataIndices, other.dataIndices ); + } } diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/ByteArrayType.java b/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/ByteArrayType.java new file mode 100644 index 0000000000..7cc6e57300 --- /dev/null +++ b/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/ByteArrayType.java @@ -0,0 +1,166 @@ +package ubic.gemma.persistence.hibernate; + +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.usertype.ParameterizedType; +import org.hibernate.usertype.UserType; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.util.Assert; +import ubic.basecode.io.ByteArrayConverter; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.Properties; + +/** + * Represents a vector of scalars stored as a byte array in a single column. + *

+ * The following types are supported for the {@code arrayType} parameter: + *

+ * Other types supported by {@link ByteArrayConverter} can be added if necessary. + * @author poirigui + * @see ByteArrayConverter + */ +public class ByteArrayType implements UserType, ParameterizedType { + + private enum ByteArrayTypes { + INT( int[].class ), + DOUBLE( double[].class ); + + private final Class arrayClass; + + ByteArrayTypes( Class arrayClass ) { + this.arrayClass = arrayClass; + } + } + + private final ByteArrayConverter converter = new ByteArrayConverter(); + private final LobHandler lobHandler = new DefaultLobHandler(); + + private ByteArrayTypes arrayType; + + @Override + public int[] sqlTypes() { + return new int[] { Types.BLOB }; + } + + @Override + public Class returnedClass() { + return arrayType.arrayClass; + } + + @Override + public boolean equals( Object x, Object y ) throws HibernateException { + switch ( arrayType ) { + case INT: + return Arrays.equals( ( int[] ) x, ( int[] ) y ); + case DOUBLE: + return Arrays.equals( ( double[] ) x, ( double[] ) y ); + default: + throw unsupportedArrayType( arrayType ); + } + } + + @Override + public int hashCode( Object x ) throws HibernateException { + switch ( arrayType ) { + case INT: + return Arrays.hashCode( ( int[] ) x ); + case DOUBLE: + return Arrays.hashCode( ( double[] ) x ); + default: + throw unsupportedArrayType( arrayType ); + } + } + + @Override + public Object nullSafeGet( ResultSet rs, String[] names, SessionImplementor session, Object owner ) throws HibernateException, SQLException { + byte[] data = lobHandler.getBlobAsBytes( rs, 0 ); + if ( data != null ) { + switch ( arrayType ) { + case INT: + return converter.byteArrayToInts( data ); + case DOUBLE: + return converter.byteArrayToDoubles( data ); + default: + throw unsupportedArrayType( arrayType ); + } + } else { + return null; + } + } + + @Override + public void nullSafeSet( PreparedStatement st, Object value, int index, SessionImplementor session ) throws HibernateException, SQLException { + byte[] blob; + if ( value != null ) { + switch ( arrayType ) { + case INT: + blob = converter.intArrayToBytes( ( int[] ) value ); + break; + case DOUBLE: + blob = converter.doubleArrayToBytes( ( double[] ) value ); + break; + default: + throw unsupportedArrayType( arrayType ); + } + } else { + blob = null; + } + lobHandler.getLobCreator().setBlobAsBytes( st, index, blob ); + } + + @Override + public Object deepCopy( Object value ) throws HibernateException { + if ( value == null ) { + return null; + } + switch ( arrayType ) { + case INT: + return ( ( int[] ) value ).clone(); + case DOUBLE: + return ( ( double[] ) value ).clone(); + default: + throw unsupportedArrayType( arrayType ); + } + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble( Object value ) throws HibernateException { + return ( Serializable ) deepCopy( value ); + } + + @Override + public Object assemble( Serializable cached, Object owner ) throws HibernateException { + return deepCopy( cached ); + } + + @Override + public Object replace( Object original, Object target, Object owner ) throws HibernateException { + return deepCopy( original ); + } + + @Override + public void setParameterValues( Properties parameters ) { + Assert.isTrue( parameters != null && parameters.containsKey( "arrayType" ), + "There must be an 'arrayType' parameter in the type declaration." ); + arrayType = ByteArrayTypes.valueOf( parameters.getProperty( "arrayType" ).toUpperCase() ); + } + + private HibernateException unsupportedArrayType( ByteArrayTypes type ) { + return new HibernateException( String.format( "Unsupported array type: %s.", type ) ); + } +} diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/CompressedStringListType.java b/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/CompressedStringListType.java index 4b6abe735e..99dc092ce6 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/CompressedStringListType.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/CompressedStringListType.java @@ -1,6 +1,7 @@ package ubic.gemma.persistence.hibernate; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.lang3.StringUtils; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SessionImplementor; @@ -12,17 +13,16 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Properties; +import java.util.*; import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import static java.util.Objects.requireNonNull; @@ -82,11 +82,13 @@ public void nullSafeSet( PreparedStatement st, Object value, int index, SessionI List s = ( List ) value; Assert.isTrue( s.stream().noneMatch( k -> k.contains( delimiter ) ), String.format( "The list of strings may not contain the delimiter %s.", delimiter ) ); - try ( InputStream is = new GZIPInputStream( IOUtils.toInputStream( String.join( delimiter, s ), StandardCharsets.UTF_8 ) ) ) { - blob = IOUtils.toByteArray( is ); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try ( OutputStream out = new GZIPOutputStream( baos ) ) { + IOUtils.write( String.join( delimiter, s ), out, StandardCharsets.UTF_8 ); } catch ( IOException e ) { throw new HibernateException( e ); } + blob = baos.toByteArray(); } else { blob = null; } @@ -95,27 +97,27 @@ public void nullSafeSet( PreparedStatement st, Object value, int index, SessionI @Override public Object deepCopy( Object value ) throws HibernateException { - return value; + return value != null ? new ArrayList<>( ( List ) value ) : null; } @Override public boolean isMutable() { - return false; + return true; } @Override public Serializable disassemble( Object value ) throws HibernateException { - return ( String ) value; + return value != null ? String.join( delimiter, ( List ) value ) : null; } @Override public Object assemble( Serializable cached, Object owner ) throws HibernateException { - return cached; + return cached != null ? Arrays.asList( StringUtils.split( ( String ) cached, delimiter ) ) : null; } @Override public Object replace( Object original, Object target, Object owner ) throws HibernateException { - return original; + return deepCopy( original ); } @Override diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/IntArrayType.java b/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/IntArrayType.java deleted file mode 100644 index f898e4d25b..0000000000 --- a/gemma-core/src/main/java/ubic/gemma/persistence/hibernate/IntArrayType.java +++ /dev/null @@ -1,94 +0,0 @@ -package ubic.gemma.persistence.hibernate; - -import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.usertype.UserType; -import org.springframework.jdbc.support.lob.DefaultLobHandler; -import org.springframework.jdbc.support.lob.LobHandler; -import ubic.basecode.io.ByteArrayConverter; - -import java.io.Serializable; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; -import java.util.Arrays; - -/** - * Represents a vector of integers stored in a single column. - * @author poirigui - * @see ByteArrayConverter#byteArrayToInts(byte[]) - * @see ByteArrayConverter#intArrayToBytes(int[]) - */ -public class IntArrayType implements UserType { - - private static final ByteArrayConverter converter = new ByteArrayConverter(); - - private final LobHandler lobHandler = new DefaultLobHandler(); - - @Override - public int[] sqlTypes() { - return new int[] { Types.BLOB }; - } - - @Override - public Class returnedClass() { - return int[].class; - } - - @Override - public boolean equals( Object x, Object y ) throws HibernateException { - return Arrays.equals( ( int[] ) x, ( int[] ) y ); - } - - @Override - public int hashCode( Object x ) throws HibernateException { - return Arrays.hashCode( ( int[] ) x ); - } - - @Override - public Object nullSafeGet( ResultSet rs, String[] names, SessionImplementor session, Object owner ) throws HibernateException, SQLException { - byte[] data = lobHandler.getBlobAsBytes( rs, 0 ); - if ( data != null ) { - return converter.byteArrayToInts( data ); - } else { - return null; - } - } - - @Override - public void nullSafeSet( PreparedStatement st, Object value, int index, SessionImplementor session ) throws HibernateException, SQLException { - byte[] blob; - if ( value != null ) { - blob = converter.intArrayToBytes( ( int[] ) value ); - } else { - blob = null; - } - lobHandler.getLobCreator().setBlobAsBytes( st, index, blob ); - } - - @Override - public Object deepCopy( Object value ) throws HibernateException { - return ( ( int[] ) value ).clone(); - } - - @Override - public boolean isMutable() { - return true; - } - - @Override - public Serializable disassemble( Object value ) throws HibernateException { - return ( int[] ) value; - } - - @Override - public Object assemble( Serializable cached, Object owner ) throws HibernateException { - return cached; - } - - @Override - public Object replace( Object original, Object target, Object owner ) throws HibernateException { - return ( ( int[] ) original ).clone(); - } -} diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java index e23278f8df..7a614ed2c0 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java @@ -12,6 +12,7 @@ import ubic.gemma.model.expression.bioAssay.BioAssay; import ubic.gemma.model.expression.bioAssayData.BioAssayDimension; import ubic.gemma.model.expression.bioAssayData.MeanVarianceRelation; +import ubic.gemma.model.expression.bioAssayData.SingleCellDimension; import ubic.gemma.model.expression.biomaterial.BioMaterial; import ubic.gemma.model.expression.experiment.*; import ubic.gemma.model.genome.Gene; @@ -304,4 +305,8 @@ Map> getSampleRemovalEvents( * The result is stored in the standard query cache. */ long countBioMaterials( @Nullable Filters filters ); + + void createSingleCellDimension( ExpressionExperiment ee, SingleCellDimension singleCellDimension ); + + void deleteSingleCellDimension( ExpressionExperiment ee, SingleCellDimension singleCellDimension ); } diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java index 7d83798372..25b06090a0 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java @@ -46,6 +46,7 @@ import ubic.gemma.model.expression.bioAssay.BioAssay; import ubic.gemma.model.expression.bioAssayData.BioAssayDimension; import ubic.gemma.model.expression.bioAssayData.MeanVarianceRelation; +import ubic.gemma.model.expression.bioAssayData.SingleCellDimension; import ubic.gemma.model.expression.biomaterial.BioMaterial; import ubic.gemma.model.expression.experiment.*; import ubic.gemma.model.genome.Gene; @@ -1944,6 +1945,16 @@ public void thawForFrontEnd( final ExpressionExperiment expressionExperiment ) { } } + @Override + public void createSingleCellDimension( ExpressionExperiment ee, SingleCellDimension singleCellDimension ) { + getSessionFactory().getCurrentSession().persist( singleCellDimension ); + } + + @Override + public void deleteSingleCellDimension( ExpressionExperiment ee, SingleCellDimension singleCellDimension ) { + getSessionFactory().getCurrentSession().delete( singleCellDimension ); + } + @Override protected Query getFilteringQuery( @Nullable Filters filters, @Nullable Sort sort ) { // the constants for aliases are messing with the inspector diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentServiceImpl.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentServiceImpl.java index de136a4643..ee2f094de8 100755 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentServiceImpl.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentServiceImpl.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.math3.exception.NotStrictlyPositiveException; import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; @@ -59,6 +60,7 @@ import ubic.gemma.model.expression.bioAssay.BioAssay; import ubic.gemma.model.expression.bioAssayData.*; import ubic.gemma.model.expression.biomaterial.BioMaterial; +import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.*; import ubic.gemma.model.genome.Gene; import ubic.gemma.model.genome.Taxon; @@ -69,6 +71,7 @@ import ubic.gemma.persistence.service.analysis.expression.pca.PrincipalComponentAnalysisService; import ubic.gemma.persistence.service.analysis.expression.sampleCoexpression.SampleCoexpressionAnalysisService; import ubic.gemma.persistence.service.common.auditAndSecurity.AuditEventService; +import ubic.gemma.persistence.service.common.auditAndSecurity.AuditTrailService; import ubic.gemma.persistence.service.common.quantitationtype.QuantitationTypeService; import ubic.gemma.persistence.service.expression.bioAssayData.BioAssayDimensionService; import ubic.gemma.persistence.util.*; @@ -101,6 +104,8 @@ public class ExpressionExperimentServiceImpl @Autowired private AuditEventService auditEventService; @Autowired + private AuditTrailService auditTrailService; + @Autowired private BioAssayDimensionService bioAssayDimensionService; @Autowired private DifferentialExpressionAnalysisService differentialExpressionAnalysisService; @@ -1300,51 +1305,93 @@ public ExpressionExperiment replaceRawVectors( ExpressionExperiment ee, @Override @Transactional public void addSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quantitationType, Collection vectors ) { + Assert.notNull( ee.getId() ); + Assert.notNull( quantitationType.getId(), "The quantitation type must be persistent." ); Assert.isTrue( !ee.getQuantitationTypes().contains( quantitationType ), - ee + " already have vectors for the quantitation type: " + quantitationType ); - Assert.isTrue( !vectors.isEmpty(), "At least one single-cell vector has to be supplied." ); - Assert.isTrue( vectors.stream().allMatch( v -> v.getQuantitationType().equals( quantitationType ) ), - "All vectors must have the quantitation type: " + quantitationType ); - Assert.isTrue( vectors.stream().map( SingleCellExpressionDataVector::getSingleCellDimension ).distinct().count() == 1, - "All vectors must share the same dimension." ); - validateSingleCellDimension( ee, vectors.iterator().next().getSingleCellDimension() ); - ExpressionExperiment finalEe = ee; - Assert.isTrue( vectors.stream().allMatch( v -> v.getExpressionExperiment() == null || v.getExpressionExperiment().equals( finalEe ) ), - "Some of the vectors belong to other expression experiments." ); - ee = ensureInSession( ee ); + String.format( "%s already have vectors for the quantitation type: %s; use replaceSingleCellDataVectors() to replace existing vectors.", + ee, quantitationType ) ); + validateSingleCellDataVectors( ee, quantitationType, vectors ); + if ( vectors.iterator().next().getSingleCellDimension().getId() == null ) { + log.info( "Creating a new single-cell dimension for " + ee ); + expressionExperimentDao.createSingleCellDimension( ee, vectors.iterator().next().getSingleCellDimension() ); + } for ( SingleCellExpressionDataVector v : vectors ) { v.setExpressionExperiment( ee ); } + int previousSize = ee.getSingleCellExpressionDataVectors().size(); + log.info( String.format( "Adding %d single-cell vectors to %s for %s", vectors.size(), ee, quantitationType ) ); ee.getSingleCellExpressionDataVectors().addAll( vectors ); + int numVectorsAdded = ee.getSingleCellExpressionDataVectors().size() - previousSize; // make all other single-cell QTs non-preferred if ( quantitationType.getIsPreferred() ) { - ee.getQuantitationTypes().forEach( q -> q.setIsPreferred( false ) ); + for ( QuantitationType qt : ee.getQuantitationTypes() ) { + if ( qt.getIsPreferred() ) { + log.info( "Setting " + qt + " to non-preferred since we're adding a new set of preferred vectors to " + ee ); + qt.setIsPreferred( false ); + break; // there is at most 1 set of preferred vectors + } + } } ee.getQuantitationTypes().add( quantitationType ); update( ee ); // will take care of creating vectors + auditTrailService.addUpdateEvent( ee, DataAddedEvent.class, + String.format( "Added %d vectors for %s", numVectorsAdded, quantitationType ) ); } @Override @Transactional public void replaceSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quantitationType, Collection vectors ) { + Assert.notNull( ee.getId() ); + Assert.notNull( quantitationType.getId(), "The quantitation type must be persistent." ); Assert.isTrue( ee.getQuantitationTypes().contains( quantitationType ), - ee + " does not have the quantitation type: " + quantitationType ); - Assert.isTrue( !vectors.isEmpty(), "At least one single-cell vector has to be supplied; use removeSingleCelLDataVectors() to remove vectors instead." ); - Assert.isTrue( vectors.stream().allMatch( v -> v.getQuantitationType().equals( quantitationType ) ), - "All vectors must have the quantitation type: " + quantitationType ); - Assert.isTrue( vectors.stream().map( SingleCellExpressionDataVector::getSingleCellDimension ).distinct().count() == 1, - "All vectors must share the same dimension." ); - validateSingleCellDimension( ee, vectors.iterator().next().getSingleCellDimension() ); - ExpressionExperiment finalEe = ee; - Assert.isTrue( vectors.stream().allMatch( v -> v.getExpressionExperiment() == null || v.getExpressionExperiment().equals( finalEe ) ), - "Some of the vectors belong to other expression experiments." ); - ee = ensureInSession( ee ); - ee.getSingleCellExpressionDataVectors().removeIf( v -> v.getQuantitationType().equals( quantitationType ) ); + String.format( "%s does not have the quantitation type: %s; use addSingleCellDataVectors() to add new vectors instead.", + ee, quantitationType ) ); + validateSingleCellDataVectors( ee, quantitationType, vectors ); + boolean scdCreated = false; + if ( vectors.iterator().next().getSingleCellDimension().getId() == null ) { + log.info( "Creating a new single-cell dimension for " + ee ); + expressionExperimentDao.createSingleCellDimension( ee, vectors.iterator().next().getSingleCellDimension() ); + scdCreated = true; + } + Set vectorsToBeReplaced = ee.getSingleCellExpressionDataVectors().stream() + .filter( v -> v.getQuantitationType().equals( quantitationType ) ).collect( Collectors.toSet() ); for ( SingleCellExpressionDataVector v : vectors ) { v.setExpressionExperiment( ee ); } + int previousSize = ee.getSingleCellExpressionDataVectors().size(); + if ( !vectorsToBeReplaced.isEmpty() ) { + // if the SCD was created, we do not need to check additional vectors for removing the existing one + removeSingleCellVectorsAndDimensionIfNecessary( ee, vectorsToBeReplaced, scdCreated ? null : vectors ); + } else { + log.warn( "No vectors with the quantitation type: " + quantitationType ); + } + int numVectorsRemoved = ee.getSingleCellExpressionDataVectors().size() - previousSize; + log.info( String.format( "Adding %d single-cell vectors to %s for %s", vectors.size(), ee, quantitationType ) ); ee.getSingleCellExpressionDataVectors().addAll( vectors ); + int numVectorsAdded = ee.getSingleCellExpressionDataVectors().size() - ( previousSize - numVectorsRemoved ); update( ee ); + auditTrailService.addUpdateEvent( ee, DataReplacedEvent.class, + String.format( "Replaced %d vectors with %d vectors for %s.", numVectorsRemoved, numVectorsAdded, quantitationType ) ); + } + + private void validateSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quantitationType, Collection vectors ) { + Assert.notNull( quantitationType.getId(), "The quantitation type must be persistent." ); + Assert.isTrue( !vectors.isEmpty(), "At least one single-cell vector has to be supplied; use removeSingleCellDataVectors() to remove vectors instead." ); + Assert.isTrue( vectors.stream().allMatch( v -> v.getExpressionExperiment() == null || v.getExpressionExperiment().equals( ee ) ), + "Some of the vectors belong to other expression experiments." ); + Assert.isTrue( vectors.stream().allMatch( v -> v.getQuantitationType() == quantitationType ), + "All vectors must have the same quantitation type: " + quantitationType ); + Assert.isTrue( vectors.stream().allMatch( v -> v.getDesignElement() != null && v.getDesignElement().getId() != null ), + "All vectors must have a persistent design element." ); + // TODO: allow vectors from multiple platforms + CompositeSequence element = vectors.iterator().next().getDesignElement(); + ArrayDesign platform = element.getArrayDesign(); + Assert.isTrue( vectors.stream().allMatch( v -> v.getDesignElement().getArrayDesign().equals( platform ) ), + "All vectors must have a persistent design element from the same platform." ); + SingleCellDimension singleCellDimension = vectors.iterator().next().getSingleCellDimension(); + validateSingleCellDimension( ee, singleCellDimension ); + Assert.isTrue( vectors.stream().allMatch( v -> v.getSingleCellDimension() == singleCellDimension ), + "All vectors must share the same dimension: " + singleCellDimension ); } /** @@ -1354,24 +1401,80 @@ private void validateSingleCellDimension( ExpressionExperiment ee, SingleCellDim Assert.isTrue( scbad.getCellIds().size() == scbad.getNumberOfCells(), "The number of cell IDs must match the number of cells." ); if ( scbad.getCellTypes() != null ) { - Assert.notNull( scbad.getNumberOfCellTypes() ); - Assert.isTrue( scbad.getCellTypes().stream().distinct().count() == scbad.getNumberOfCellTypes(), - "The number of cell types must match the number of distinct values the cellTypes collection." ); + Assert.notNull( scbad.getNumberOfCellTypeLabels() ); + Assert.notNull( scbad.getCellTypeLabels() ); + Assert.isTrue( scbad.getCellTypes().length == scbad.getCellIds().size(), + "The number of cell types must match the number of cell IDs." ); + Assert.isTrue( scbad.getCellTypeLabels().size() == scbad.getNumberOfCellTypeLabels(), + "The number of cell types must match the number of values the cellTypeLabels collection." ); } else { - Assert.isNull( scbad.getNumberOfCellTypes(), "There is no cell types assigned, the number of cell types must be null." ); + Assert.isNull( scbad.getCellTypeLabels() ); + Assert.isNull( scbad.getNumberOfCellTypeLabels(), "There is no cell types assigned, the number of cell types must be null." ); } Assert.isTrue( ee.getBioAssays().containsAll( scbad.getBioAssays() ), "Not all supplied BioAssays belong to " + ee ); - validateSparseRangeArray( scbad.getBioAssays(), scbad.getBioAssaysOffset(), scbad.getNumberOfCells() ); + validateSparseRangeArray( scbad.getBioAssays(), scbad.getBioAssaysOffset(), scbad.getNumberOfCells() ); } @Override @Transactional public void removeSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quantitationType ) { + Assert.notNull( ee.getId() ); + Assert.notNull( quantitationType.getId() ); Assert.isTrue( ee.getQuantitationTypes().contains( quantitationType ) ); - ee = ensureInSession( ee ); - ee.getSingleCellExpressionDataVectors().removeIf( v -> v.getQuantitationType().equals( quantitationType ) ); + Set vectors = ee.getSingleCellExpressionDataVectors().stream() + .filter( v -> v.getQuantitationType().equals( quantitationType ) ).collect( Collectors.toSet() ); + if ( !vectors.isEmpty() ) { + removeSingleCellVectorsAndDimensionIfNecessary( ee, vectors, null ); + } else { + log.warn( "No vectors with the quantitation type: " + quantitationType ); + } ee.getQuantitationTypes().remove( quantitationType ); update( ee ); + auditTrailService.addUpdateEvent( ee, DataRemovedEvent.class, + String.format( "Removed %d vectors for %s.", vectors.size(), quantitationType ) ); + } + + /** + * @deprecated do not use this, it's only meant as a workaround for deleting single-cell vectors + */ + @Autowired + @Deprecated + private SessionFactory sessionFactory; + + /** + * Remove the given single-cell vectors and their corresponding single-cell dimension if necessary. + * @param ee the experiment to remove the vectors from. + * @param additionalVectors additional vectors to check if the single-cell dimension is still in use (i.e. vectors that are in the process of being added). + * @return true if the vectors were removed, false otherwise. + */ + private void removeSingleCellVectorsAndDimensionIfNecessary( ExpressionExperiment ee, + Collection vectors, + @Nullable Collection additionalVectors ) { + log.info( String.format( "Removing %d single-cell vectors for %s...", vectors.size(), ee ) ); + ee.getSingleCellExpressionDataVectors().removeAll( vectors ); + // FIXME: flushing shouldn't be necessary here, but Hibernate does appear to cascade vectors removal prior to removing the SCD or QT... + sessionFactory.getCurrentSession().flush(); + // check if SCD is still in use else remove it + SingleCellDimension scd = vectors.iterator().next().getSingleCellDimension(); + boolean scdStillUsed = false; + for ( SingleCellExpressionDataVector v : ee.getSingleCellExpressionDataVectors() ) { + if ( v.getSingleCellDimension().equals( scd ) ) { + scdStillUsed = true; + break; + } + } + if ( !scdStillUsed && additionalVectors != null ) { + for ( SingleCellExpressionDataVector v : additionalVectors ) { + if ( v.getSingleCellDimension().equals( scd ) ) { + scdStillUsed = true; + break; + } + } + } + if ( !scdStillUsed ) { + log.info( "Removing unused single-cell dimension " + scd + " for " + ee ); + expressionExperimentDao.deleteSingleCellDimension( ee, scd ); + } } /** diff --git a/gemma-core/src/main/resources/hibernate.cfg.xml b/gemma-core/src/main/resources/hibernate.cfg.xml index c6fbfa9bb5..8916d216b7 100644 --- a/gemma-core/src/main/resources/hibernate.cfg.xml +++ b/gemma-core/src/main/resources/hibernate.cfg.xml @@ -1,7 +1,7 @@ + "-//Hibernate/Hibernate Configuration DTD//EN" + "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> @@ -22,7 +22,8 @@ - + @@ -63,6 +64,8 @@ + + diff --git a/gemma-core/src/main/resources/ubic/gemma/model/analysis/Investigation.hbm.xml b/gemma-core/src/main/resources/ubic/gemma/model/analysis/Investigation.hbm.xml index dd755fde41..fa6a80f3a9 100644 --- a/gemma-core/src/main/resources/ubic/gemma/model/analysis/Investigation.hbm.xml +++ b/gemma-core/src/main/resources/ubic/gemma/model/analysis/Investigation.hbm.xml @@ -128,7 +128,8 @@ - + diff --git a/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.hbm.xml b/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.hbm.xml index b1edcc6307..92c2c81d6a 100644 --- a/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.hbm.xml +++ b/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDimension.hbm.xml @@ -5,7 +5,7 @@ + table="SINGLE_CELL_DIMENSION" mutable="false"> @@ -22,11 +22,20 @@ - - \n + + int - + + + + + + + + + + @@ -35,13 +44,15 @@ - + - + + int + diff --git a/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDataVector.hbm.xml b/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.hbm.xml similarity index 79% rename from gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDataVector.hbm.xml rename to gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.hbm.xml index 63f43b7f50..eb39a12565 100644 --- a/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellDataVector.hbm.xml +++ b/gemma-core/src/main/resources/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVector.hbm.xml @@ -5,10 +5,11 @@ + table="SINGLE_CELL_EXPRESSION_DATA_VECTOR"> - + + @@ -21,15 +22,18 @@ - + + + int + - + + lazy="proxy" fetch="select"> diff --git a/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/bioAssayData/SingleCellExpressionDataVectorDaoTest.java b/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/bioAssayData/SingleCellExpressionDataVectorDaoTest.java deleted file mode 100644 index 069d3bb1fb..0000000000 --- a/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/bioAssayData/SingleCellExpressionDataVectorDaoTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package ubic.gemma.persistence.service.expression.bioAssayData; - -import org.hibernate.SessionFactory; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import ubic.basecode.io.ByteArrayConverter; -import ubic.gemma.core.util.test.BaseDatabaseTest; -import ubic.gemma.model.expression.bioAssay.BioAssay; -import ubic.gemma.model.expression.bioAssayData.SingleCellDimension; -import ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVector; -import ubic.gemma.persistence.util.TestComponent; - -import java.util.Arrays; -import java.util.Collections; - -@ContextConfiguration -public class SingleCellExpressionDataVectorDaoTest extends BaseDatabaseTest { - - private static final ByteArrayConverter byteArrayConverter = new ByteArrayConverter(); - - @Configuration - @TestComponent - static class SingleCellDataVectorDaoTestContextConfiguration extends BaseDatabaseTestContextConfiguration { - - } - - @Autowired - private SessionFactory sessionFactory; - - @Test - public void test() { - BioAssay b1 = new BioAssay(); - - SingleCellDimension dimension = new SingleCellDimension(); - dimension.setCellIds( Arrays.asList( "cell1", "cell2", "cell3" ) ); - dimension.setBioAssays( Collections.singletonList( b1 ) ); - dimension.setBioAssaysOffset( new int[] { 0 } ); - - SingleCellExpressionDataVector vector = new SingleCellExpressionDataVector(); - vector.setSingleCellDimension( dimension ); - vector.setData( byteArrayConverter.doubleArrayToBytes( new double[] { 1.0f, 2.0f, 3.0f } ) ); - vector.setDataIndices( new int[] { 1, 5, 8 } ); - sessionFactory.getCurrentSession().persist( vector ); - } -} \ No newline at end of file diff --git a/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceTest.java b/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceTest.java index 55bae3cf13..4bc15030ca 100644 --- a/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceTest.java +++ b/gemma-core/src/test/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceTest.java @@ -1,33 +1,352 @@ package ubic.gemma.persistence.service.expression.experiment; -import ubic.gemma.model.common.quantitationtype.QuantitationType; +import gemma.gsec.SecurityService; +import org.hibernate.SessionFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.test.context.ContextConfiguration; +import ubic.gemma.core.analysis.preprocess.svd.SVDService; +import ubic.gemma.core.ontology.OntologyService; +import ubic.gemma.core.search.SearchService; +import ubic.gemma.core.util.test.BaseDatabaseTest; +import ubic.gemma.model.common.auditAndSecurity.eventType.DataAddedEvent; +import ubic.gemma.model.common.auditAndSecurity.eventType.DataRemovedEvent; +import ubic.gemma.model.common.auditAndSecurity.eventType.DataReplacedEvent; +import ubic.gemma.model.common.quantitationtype.*; +import ubic.gemma.model.expression.arrayDesign.ArrayDesign; +import ubic.gemma.model.expression.bioAssayData.SingleCellDimension; import ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVector; +import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.ExpressionExperiment; +import ubic.gemma.model.genome.Taxon; +import ubic.gemma.persistence.service.analysis.expression.coexpression.CoexpressionAnalysisService; +import ubic.gemma.persistence.service.analysis.expression.diff.DifferentialExpressionAnalysisService; +import ubic.gemma.persistence.service.analysis.expression.pca.PrincipalComponentAnalysisService; +import ubic.gemma.persistence.service.analysis.expression.sampleCoexpression.SampleCoexpressionAnalysisService; +import ubic.gemma.persistence.service.common.auditAndSecurity.AuditEventService; +import ubic.gemma.persistence.service.common.auditAndSecurity.AuditTrailService; +import ubic.gemma.persistence.service.common.quantitationtype.QuantitationTypeService; +import ubic.gemma.persistence.service.expression.bioAssayData.BioAssayDimensionService; +import ubic.gemma.persistence.util.TestComponent; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.mockito.internal.verification.VerificationModeFactory.only; + /** * Tests covering integration of single-cell. */ -public class SingleCellExpressionExperimentServiceTest { +@ContextConfiguration +public class SingleCellExpressionExperimentServiceTest extends BaseDatabaseTest { + + @Configuration + @TestComponent + static class SingleCellExpressionExperimentServiceTestContextConfiguration extends BaseDatabaseTestContextConfiguration { + + @Bean + public ExpressionExperimentService expressionExperimentService( ExpressionExperimentDao expressionExperimentDao ) { + return new ExpressionExperimentServiceImpl( expressionExperimentDao ); + } + + @Bean + public ExpressionExperimentDao expressionExperimentDao( SessionFactory sessionFactory ) { + return new ExpressionExperimentDaoImpl( sessionFactory ); + } + + @Bean + public AuditEventService auditEventService() { + return mock( AuditEventService.class ); + } + + @Bean + public AuditTrailService auditTrailService() { + return mock( AuditTrailService.class ); + } + + @Bean + public BioAssayDimensionService bioAssayDimensionService() { + return mock( BioAssayDimensionService.class ); + } + + @Bean + public DifferentialExpressionAnalysisService differentialExpressionAnalysisService() { + return mock( DifferentialExpressionAnalysisService.class ); + } + + @Bean + public ExpressionExperimentSetService expressionExperimentSetService() { + return mock( ExpressionExperimentSetService.class ); + } + + @Bean + public ExpressionExperimentSubSetService expressionExperimentSubSetService() { + return mock( ExpressionExperimentSubSetService.class ); + } + + @Bean + public ExperimentalFactorService experimentalFactorService() { + return mock( ExperimentalFactorService.class ); + } + + @Bean + public FactorValueService factorValueService() { + return mock( FactorValueService.class ); + } + + @Bean + public OntologyService ontologyService() { + return mock( OntologyService.class ); + } + + @Bean + public PrincipalComponentAnalysisService principalComponentAnalysisService() { + return mock( PrincipalComponentAnalysisService.class ); + } + + @Bean + public QuantitationTypeService quantitationTypeService() { + return mock( QuantitationTypeService.class ); + } + + @Bean + public SearchService searchService() { + return mock( SearchService.class ); + } + @Bean + public SecurityService securityService() { + return mock( SecurityService.class ); + } + + @Bean + public SVDService svdService() { + return mock( SVDService.class ); + } + + @Bean + public CoexpressionAnalysisService coexpressionAnalysisService() { + return mock( CoexpressionAnalysisService.class ); + } + + @Bean + public SampleCoexpressionAnalysisService sampleCoexpressionAnalysisService() { + return mock( SampleCoexpressionAnalysisService.class ); + } + + @Bean + public BlacklistedEntityService blacklistedEntityService() { + return mock( BlacklistedEntityService.class ); + } + + @Bean + public AccessDecisionManager accessDecisionManager() { + return mock( AccessDecisionManager.class ); + } + } + + @Autowired private ExpressionExperimentService expressionExperimentService; - public void testAddPreferredVectors() { - ExpressionExperiment ee = new ExpressionExperiment(); - QuantitationType existingQt = new QuantitationType(); - existingQt.setIsPreferred( true ); - ee.getQuantitationTypes().add( existingQt ); + @Autowired + private AuditTrailService auditTrailService; + + private ArrayDesign ad; + private ExpressionExperiment ee; + @Before + public void setUp() { + Taxon taxon = new Taxon(); + sessionFactory.getCurrentSession().persist( taxon ); + ad = new ArrayDesign(); + ad.setPrimaryTaxon( taxon ); + CompositeSequence cs = new CompositeSequence(); + cs.setName( "test" ); + cs.setArrayDesign( ad ); + ad.getCompositeSequences().add( cs ); + sessionFactory.getCurrentSession().persist( ad ); + ee = new ExpressionExperiment(); + ee.setTaxon( taxon ); + ee = expressionExperimentService.create( ee ); + } + + @After + public void resetMocks() { + reset( auditTrailService ); + } + + @Test + public void testAddSingleCellDataVectors() { + Collection vectors = createSingleCellVectors( true ); + + expressionExperimentService.addSingleCellDataVectors( ee, vectors.iterator().next().getQuantitationType(), vectors ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getQuantitationTypes() ) + .contains( vectors.iterator().next().getQuantitationType() ); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ) + .allSatisfy( v -> assertThat( v.getId() ).isNotNull() ); + + Collection vectors2 = createSingleCellVectors( true ); + expressionExperimentService.addSingleCellDataVectors( ee, vectors2.iterator().next().getQuantitationType(), vectors2 ); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 2 ); + + verify( auditTrailService, times( 2 ) ).addUpdateEvent( eq( ee ), eq( DataAddedEvent.class ), any() ); + } + + @Test + public void testAddSingleCellDataVectorsTwice() { + Collection vectors = createSingleCellVectors( true ); + expressionExperimentService.addSingleCellDataVectors( ee, vectors.iterator().next().getQuantitationType(), vectors ); + sessionFactory.getCurrentSession().flush(); + assertThatThrownBy( () -> expressionExperimentService.addSingleCellDataVectors( ee, vectors.iterator().next().getQuantitationType(), vectors ) ) + .isInstanceOf( IllegalArgumentException.class ) + .hasMessageContaining( "already have vectors for the quantitation type" ); + verify( auditTrailService, only() ).addUpdateEvent( eq( ee ), eq( DataAddedEvent.class ), any() ); + } + + @Test + public void testAddSingleCellDataVectorsWhenThereIsAlreadyAPreferredSetOfVectors() { + Collection vectors = createSingleCellVectors( true ); + expressionExperimentService.addSingleCellDataVectors( ee, vectors.iterator().next().getQuantitationType(), vectors ); + sessionFactory.getCurrentSession().flush(); + Collection vectors2 = createSingleCellVectors( true ); + expressionExperimentService.addSingleCellDataVectors( ee, vectors2.iterator().next().getQuantitationType(), vectors2 ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getQuantitationTypes() ) + .hasSize( 2 ) + .satisfiesOnlyOnce( qt -> assertThat( qt.getIsPreferred() ).isTrue() ); + verify( auditTrailService, times( 2 ) ).addUpdateEvent( eq( ee ), eq( DataAddedEvent.class ), any() ); + } + + @Test + public void testReplaceVectors() { + Collection vectors = createSingleCellVectors( true ); + QuantitationType qt = vectors.iterator().next().getQuantitationType(); + expressionExperimentService.addSingleCellDataVectors( ee, qt, vectors ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ); + + Collection vectors2 = createSingleCellVectors( qt ); + expressionExperimentService.replaceSingleCellDataVectors( ee, qt, vectors2 ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ) + .doesNotContainAnyElementsOf( vectors ) + .containsAll( vectors2 ); + + verify( auditTrailService ).addUpdateEvent( eq( ee ), eq( DataAddedEvent.class ), any() ); + verify( auditTrailService ).addUpdateEvent( eq( ee ), eq( DataReplacedEvent.class ), any() ); + } + + @Test + public void testRemoveVectors() { + Collection vectors = createSingleCellVectors( true ); + QuantitationType qt = vectors.iterator().next().getQuantitationType(); + expressionExperimentService.addSingleCellDataVectors( ee, qt, vectors ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ); + + Collection vectors2 = createSingleCellVectors( false ); + QuantitationType qt2 = vectors2.iterator().next().getQuantitationType(); + expressionExperimentService.addSingleCellDataVectors( ee, qt2, vectors2 ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 2 ); + + expressionExperimentService.removeSingleCellDataVectors( ee, qt ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ); + + verify( auditTrailService, times( 2 ) ).addUpdateEvent( eq( ee ), eq( DataAddedEvent.class ), any() ); + verify( auditTrailService ).addUpdateEvent( eq( ee ), eq( DataRemovedEvent.class ), any() ); + } + + @Test + public void testRemoveVectorsSharingADimension() { + Collection vectors = createSingleCellVectors( true ); + QuantitationType qt = vectors.iterator().next().getQuantitationType(); + expressionExperimentService.addSingleCellDataVectors( ee, qt, vectors ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getQuantitationTypes() ).contains( qt ); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ); + + Collection vectors2 = createSingleCellVectors( vectors.iterator().next().getSingleCellDimension() ); + QuantitationType qt2 = vectors2.iterator().next().getQuantitationType(); + expressionExperimentService.addSingleCellDataVectors( ee, qt2, vectors2 ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getQuantitationTypes() ).contains( qt2 ); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 2 ); + + expressionExperimentService.removeSingleCellDataVectors( ee, qt ); + sessionFactory.getCurrentSession().flush(); + assertThat( ee.getQuantitationTypes() ).doesNotContain( qt ); + assertThat( ee.getSingleCellExpressionDataVectors() ) + .hasSize( 1 ); + + verify( auditTrailService, times( 2 ) ).addUpdateEvent( eq( ee ), eq( DataAddedEvent.class ), any() ); + verify( auditTrailService ).addUpdateEvent( eq( ee ), eq( DataRemovedEvent.class ), any() ); + } + + private Collection createSingleCellVectors( boolean preferred ) { QuantitationType qt = new QuantitationType(); - qt.setIsPreferred( true ); + qt.setGeneralType( GeneralType.QUANTITATIVE ); + qt.setType( StandardQuantitationType.AMOUNT ); + qt.setRepresentation( PrimitiveType.DOUBLE ); + qt.setScale( ScaleType.LOG2 ); + qt.setIsPreferred( preferred ); + sessionFactory.getCurrentSession().persist( qt ); + SingleCellDimension scd = new SingleCellDimension(); + scd.setCellIds( new ArrayList<>() ); + scd.setNumberOfCells( 0 ); + return createSingleCellVectors( scd, qt ); + } + + private Collection createSingleCellVectors( QuantitationType qt ) { + SingleCellDimension scd = new SingleCellDimension(); + scd.setCellIds( new ArrayList<>() ); + scd.setNumberOfCells( 0 ); + return createSingleCellVectors( scd, qt ); + } + + private Collection createSingleCellVectors( SingleCellDimension singleCellDimension ) { + QuantitationType qt = new QuantitationType(); + qt.setGeneralType( GeneralType.QUANTITATIVE ); + qt.setType( StandardQuantitationType.AMOUNT ); + qt.setRepresentation( PrimitiveType.DOUBLE ); + qt.setScale( ScaleType.LOG2 ); + qt.setIsPreferred( false ); + sessionFactory.getCurrentSession().persist( qt ); + return createSingleCellVectors( singleCellDimension, qt ); + } + + private Collection createSingleCellVectors( SingleCellDimension scd, QuantitationType qt ) { Collection vectors = new HashSet<>(); SingleCellExpressionDataVector v = new SingleCellExpressionDataVector(); + v.setDesignElement( ad.getCompositeSequences().iterator().next() ); + v.setSingleCellDimension( scd ); v.setExpressionExperiment( ee ); v.setQuantitationType( qt ); + v.setData( new byte[0] ); + v.setDataIndices( new int[0] ); vectors.add( v ); - - expressionExperimentService.addSingleCellDataVectors( ee, qt, vectors ); + return vectors; } }