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:
+ *
+ * - {@code int}
+ * - {@code double}
+ *
+ * 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;
}
}