diff --git a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/experiment/measurement/MeasurementLookupImplementation.java b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/experiment/measurement/MeasurementLookupImplementation.java index d937c12c65..c84a679696 100644 --- a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/experiment/measurement/MeasurementLookupImplementation.java +++ b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/experiment/measurement/MeasurementLookupImplementation.java @@ -252,7 +252,7 @@ public static Specification isOrganisationLabel(String fi public static Specification isOntologyTermName(String filter) { return (root, query, builder) -> { Expression function = builder.function("JSON_EXTRACT", String.class, - root.get("instrument"), + root.get("msDevice"), builder.literal("$.name")); return builder.like(function, "%" + filter + "%"); @@ -263,7 +263,7 @@ public static Specification isOntologyTermLabel(String fi return (root, query, builder) -> { Expression function = builder.function("JSON_EXTRACT", String.class, - root.get("instrument"), builder.literal("$.label")); + root.get("msDevice"), builder.literal("$.label")); return builder.like(function, "%" + filter + "%"); }; diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/MeasurementService.java b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/MeasurementService.java index 8850df4ac1..966d1d2cab 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/MeasurementService.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/MeasurementService.java @@ -389,15 +389,20 @@ private Map> buildPxP( throw new MeasurementRegistrationException(ErrorCode.UNKNOWN_ORGANISATION_ROR_ID); } - var instrumentQuery = resolveOntologyCURI(firstMetadataEntry.instrumentCURI()); - if (instrumentQuery.isEmpty()) { + var msDeviceQuery = resolveOntologyCURI(firstMetadataEntry.msDeviceCURIE()); + if (msDeviceQuery.isEmpty()) { throw new MeasurementRegistrationException(ErrorCode.UNKNOWN_ONTOLOGY_TERM); } - var method = new ProteomicsMethodMetadata(instrumentQuery.get(), firstMetadataEntry.facility(), - firstMetadataEntry.digestionMethod(), firstMetadataEntry.digestionEnzyme(), - firstMetadataEntry.enrichmentMethod(), firstMetadataEntry.lcColumn(), - firstMetadataEntry.lcmsMethod(), readInjectionVolume(firstMetadataEntry.injectionVolume()), + var method = new ProteomicsMethodMetadata(msDeviceQuery.get(), + firstMetadataEntry.technicalReplicateName(), + firstMetadataEntry.facility(), + firstMetadataEntry.digestionMethod(), + firstMetadataEntry.digestionEnzyme(), + firstMetadataEntry.enrichmentMethod(), + firstMetadataEntry.lcColumn(), + firstMetadataEntry.lcmsMethod(), + readInjectionVolume(firstMetadataEntry.injectionVolume()), firstMetadataEntry.labeling() .labelType()); @@ -611,12 +616,13 @@ private List> updateAllPxP( throw new MeasurementRegistrationException(ErrorCode.UNKNOWN_ORGANISATION_ROR_ID); } - var instrumentQuery = resolveOntologyCURI(measurementMetadata.instrumentCURI()); - if (instrumentQuery.isEmpty()) { + var msDeviceQuery = resolveOntologyCURI(measurementMetadata.msDeviceCURIE()); + if (msDeviceQuery.isEmpty()) { throw new MeasurementRegistrationException(ErrorCode.UNKNOWN_ONTOLOGY_TERM); } - var method = new ProteomicsMethodMetadata(instrumentQuery.get(), + var method = new ProteomicsMethodMetadata(msDeviceQuery.get(), + measurementMetadata.technicalReplicateName(), measurementMetadata.facility(), measurementMetadata.digestionMethod(), measurementMetadata.digestionEnzyme(), measurementMetadata.enrichmentMethod(), measurementMetadata.lcColumn(), @@ -642,15 +648,20 @@ private List> updateAllPxP( throw new MeasurementRegistrationException(ErrorCode.UNKNOWN_ORGANISATION_ROR_ID); } - var instrumentQuery = resolveOntologyCURI(firstEntry.instrumentCURI()); - if (instrumentQuery.isEmpty()) { + var msDeviceQuery = resolveOntologyCURI(firstEntry.msDeviceCURIE()); + if (msDeviceQuery.isEmpty()) { throw new MeasurementRegistrationException(ErrorCode.UNKNOWN_ONTOLOGY_TERM); } - var method = new ProteomicsMethodMetadata(instrumentQuery.get(), firstEntry.facility(), - firstEntry.digestionMethod(), firstEntry.digestionEnzyme(), - firstEntry.enrichmentMethod(), firstEntry.lcColumn(), - firstEntry.lcmsMethod(), readInjectionVolume(firstEntry.injectionVolume()), + var method = new ProteomicsMethodMetadata(msDeviceQuery.get(), + firstEntry.technicalReplicateName(), + firstEntry.facility(), + firstEntry.digestionMethod(), + firstEntry.digestionEnzyme(), + firstEntry.enrichmentMethod(), + firstEntry.lcColumn(), + firstEntry.lcmsMethod(), + readInjectionVolume(firstEntry.injectionVolume()), firstEntry.labeling() .labelType()); diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/ProteomicsMeasurementMetadata.java b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/ProteomicsMeasurementMetadata.java index 690c434b5b..fd5811dcca 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/ProteomicsMeasurementMetadata.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/ProteomicsMeasurementMetadata.java @@ -10,38 +10,26 @@ * * @since 1.0.0 */ -public record ProteomicsMeasurementMetadata(String measurementId, - SampleCode sampleCode, - String organisationId, String instrumentCURI, - String samplePoolGroup, String facility, - String fractionName, - String digestionEnzyme, - String digestionMethod, String enrichmentMethod, - String injectionVolume, String lcColumn, - String lcmsMethod, Labeling labeling, - String comment) implements MeasurementMetadata { +public record ProteomicsMeasurementMetadata( + String measurementId, + SampleCode sampleCode, + String technicalReplicateName, + String organisationId, + String msDeviceCURIE, + String samplePoolGroup, + String facility, + String fractionName, + String digestionEnzyme, + String digestionMethod, + String enrichmentMethod, + String injectionVolume, + String lcColumn, + String lcmsMethod, + Labeling labeling, + String comment +) implements MeasurementMetadata { - public static ProteomicsMeasurementMetadata copyWithNewProperties(SampleCode associatedSample, - Labeling labeling, - ProteomicsMeasurementMetadata metadata) { - return new ProteomicsMeasurementMetadata(metadata.measurementId(), - associatedSample, - metadata.organisationId(), - metadata.instrumentCURI(), - metadata.samplePoolGroup(), - metadata.facility(), - metadata.fractionName(), - metadata.digestionEnzyme(), - metadata.digestionMethod(), - metadata.enrichmentMethod(), - metadata.injectionVolume(), - metadata.lcColumn(), - metadata.lcmsMethod(), - labeling, - metadata.comment()); - } - @Override public Optional assignedSamplePoolGroup() { return Optional.ofNullable(samplePoolGroup.isBlank() ? null : samplePoolGroup); @@ -72,4 +60,5 @@ public boolean equals(Object o) { public int hashCode() { return measurementId.hashCode(); } + } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java index c83caec572..fd0d065ee6 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java @@ -102,7 +102,7 @@ public ValidationResult validate(ProteomicsMeasurementMetadata measurementMetada return validationPolicy.validateSampleId(measurementMetadata.sampleCode()) .combine(validationPolicy.validateMandatoryDataProvided(measurementMetadata)) .combine(validationPolicy.validateOrganisation(measurementMetadata.organisationId()) - .combine(validationPolicy.validateInstrument(measurementMetadata.instrumentCURI()))); + .combine(validationPolicy.validateMsDevice(measurementMetadata.msDeviceCURIE()))); } /** @@ -121,7 +121,7 @@ public ValidationResult validateUpdate(ProteomicsMeasurementMetadata metadata, .combine(validationPolicy.validateMeasurementCode(metadata.measurementIdentifier().orElse("")) .combine(validationPolicy.validateMandatoryDataForUpdate(metadata)) .combine(validationPolicy.validateOrganisation(metadata.organisationId()) - .combine(validationPolicy.validateInstrument(metadata.instrumentCURI()) + .combine(validationPolicy.validateMsDevice(metadata.msDeviceCURIE()) .combine( validationPolicy.validateDigestionMethod(metadata.digestionMethod()))))); } @@ -129,9 +129,10 @@ public ValidationResult validateUpdate(ProteomicsMeasurementMetadata metadata, public enum PROTEOMICS_PROPERTY { QBIC_SAMPLE_ID("qbic sample id"), SAMPLE_LABEL("sample name"), + TECHNICAL_REPLICATE_NAME("technical replicate"), ORGANISATION_ID("organisation id"), FACILITY("facility"), - INSTRUMENT("instrument"), + MS_DEVICE("ms device"), SAMPLE_POOL_GROUP("sample pool group"), CYCLE_FRACTION_NAME("cycle/fraction name"), DIGESTION_METHOD("digestion method"), @@ -180,6 +181,10 @@ public static boolean isDigestionMethod(String input) { public String getName() { return name; } + + public static List getOptions() { + return Arrays.stream(values()).map(DigestionMethod::getName).toList(); + } } private class ValidationPolicy { @@ -188,7 +193,7 @@ private class ValidationPolicy { private static final String UNKNOWN_ORGANISATION_ID_MESSAGE = "The organisation ID does not seem to be a ROR ID: \"%s\""; - private static final String UNKNOWN_INSTRUMENT_ID = "Unknown instrument id: \"%s\""; + private static final String UNKNOWN_MS_DEVICE_ID = "Unknown ms device id: \"%s\""; private static final String UNKNOWN_DIGESTION_METHOD = "Unknown digestion method: \"%s\""; @@ -242,13 +247,13 @@ ValidationResult validateMeasurementCode(String measurementCode) { List.of("Measurement Code: Unknown measurement for id '%s'".formatted(measurementCode)))); } - ValidationResult validateInstrument(String instrument) { - var result = terminologyService.findByCurie(instrument); + ValidationResult validateMsDevice(String msDevice) { + var result = terminologyService.findByCurie(msDevice); if (result.isPresent()) { return ValidationResult.successful(1); } return ValidationResult.withFailures(1, - List.of(UNKNOWN_INSTRUMENT_ID.formatted(instrument))); + List.of(UNKNOWN_MS_DEVICE_ID.formatted(msDevice))); } ValidationResult validateDigestionMethod(String digestionMethod) { @@ -273,9 +278,9 @@ ValidationResult validateMandatoryDataForUpdate(ProteomicsMeasurementMetadata me } else { validation = validation.combine(ValidationResult.successful(1)); } - if (metadata.instrumentCURI().isBlank()) { + if (metadata.msDeviceCURIE().isBlank()) { validation = validation.combine( - ValidationResult.withFailures(1, List.of("Instrument: missing mandatory metadata"))); + ValidationResult.withFailures(1, List.of("MS Device: missing mandatory metadata"))); } else { validation = validation.combine(ValidationResult.successful(1)); } @@ -323,10 +328,10 @@ ValidationResult validateMandatoryDataProvided( } else { validation = validation.combine(ValidationResult.successful(1)); } - if (metadata.instrumentCURI().isBlank()) { + if (metadata.msDeviceCURIE().isBlank()) { validation = validation.combine( ValidationResult.withFailures(1, - List.of("Instrument: missing mandatory metadata"))); + List.of("MS Device: missing mandatory metadata"))); } else { validation = validation.combine(ValidationResult.successful(1)); } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java index dc6f1deb84..2c8f7455de 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/NGSMeasurement.java @@ -18,8 +18,8 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import life.qbic.domain.concepts.LocalDomainEventDispatcher; import java.util.Set; +import life.qbic.domain.concepts.LocalDomainEventDispatcher; import life.qbic.projectmanagement.domain.Organisation; import life.qbic.projectmanagement.domain.model.OntologyTerm; import life.qbic.projectmanagement.domain.model.measurement.event.MeasurementCreatedEvent; @@ -93,7 +93,7 @@ private NGSMeasurement(MeasurementId measurementId, ProjectId projectId, this.measurementId = measurementId; this.projectId = requireNonNull(projectId, "projectId must not be null"); this.organisation = requireNonNull(organisation, "organisation must not be null"); - this.instrument = requireNonNull(method.instrument(), "instrument must not be null"); + this.instrument = requireNonNull(method.instrument(), "msDevice must not be null"); this.measurementCode = requireNonNull(measurementCode, "measurement code must not be null"); this.facility = requireNonNull(method.facility(), "facility must not be null"); this.sequencingReadType = requireNonNull(method.sequencingReadType(), @@ -136,7 +136,7 @@ public static NGSMeasurement createWithPool(ProjectId projectId, String samplePo throws IllegalArgumentException { requireNonNull(measurementCode, "measurement Code must not be null"); requireNonNull(method, "method must not be null"); - requireNonNull(method.instrument(), "instrument must not be null"); + requireNonNull(method.instrument(), "msDevice must not be null"); if (samplePool.isBlank()) { throw new IllegalArgumentException("Sample Pool: no value provided"); } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java index dc8470580f..eefc2a1fc4 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMeasurement.java @@ -57,7 +57,10 @@ public class ProteomicsMeasurement { private MeasurementId measurementId; @Column(name = "instrument", columnDefinition = "longtext CHECK (json_valid(`instrument`))") - private OntologyTerm instrument; + private OntologyTerm msDevice; + + @Column(name = "technicalReplicateName") + private String technicalReplicateName; @Column(name = "samplePool") private String samplePool = ""; @@ -127,8 +130,8 @@ private static void evaluateMandatorySpecificMetadata( private static void evaluateMandatoryMetadata(ProteomicsMethodMetadata method) throws IllegalArgumentException { - if (method.instrument() == null) { - throw new IllegalArgumentException("Instrument: Missing metadata."); + if (method.msDevice() == null) { + throw new IllegalArgumentException("MS Device: Missing metadata."); } if (method.facility().isBlank()) { throw new IllegalArgumentException("Facility: Missing metadata"); @@ -157,7 +160,7 @@ public static ProteomicsMeasurement create(ProjectId projectId, MeasurementCode measurementCode, Organisation organisation, ProteomicsMethodMetadata method, Collection proteomicsSpecificMeasurementMetadata) throws IllegalArgumentException { - requireNonNull(method.instrument()); + requireNonNull(method.msDevice()); requireNonNull(measurementCode); requireNonNull(proteomicsSpecificMeasurementMetadata); if (!measurementCode.isMSDomain()) { @@ -215,8 +218,8 @@ public Collection measuredSamples() { .toList(); } - public OntologyTerm instrument() { - return instrument; + public OntologyTerm msDevice() { + return msDevice; } public Organisation organisation() { @@ -257,7 +260,8 @@ public void updateMethod(ProteomicsMethodMetadata method) { } private void setMethodMetadata(ProteomicsMethodMetadata methodMetadata) { - this.instrument = methodMetadata.instrument(); + this.msDevice = methodMetadata.msDevice(); + this.technicalReplicateName = methodMetadata.technicalReplicate(); this.facility = methodMetadata.facility(); this.digestionMethod = methodMetadata.digestionMethod(); this.digestionEnzyme = methodMetadata.digestionEnzyme(); @@ -319,4 +323,8 @@ public int hashCode() { public Optional comment() { return Optional.empty(); } + + public Optional technicalReplicateName() { + return Optional.ofNullable(technicalReplicateName); + } } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMethodMetadata.java b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMethodMetadata.java index c841db0764..c8ecef142c 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMethodMetadata.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/domain/model/measurement/ProteomicsMethodMetadata.java @@ -9,10 +9,17 @@ * * @since 1.0.0 */ -public record ProteomicsMethodMetadata(OntologyTerm instrument, String facility, - String digestionMethod, - String digestionEnzyme, String enrichmentMethod, - String lcColumn, String lcmsMethod, int injectionVolume, - String labelType) { +public record ProteomicsMethodMetadata( + OntologyTerm msDevice, + String technicalReplicate, + String facility, + String digestionMethod, + String digestionEnzyme, + String enrichmentMethod, + String lcColumn, + String lcmsMethod, + int injectionVolume, + String labelType +) { } diff --git a/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy b/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy index cc2787c58b..12b4bff779 100644 --- a/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy +++ b/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy @@ -16,6 +16,7 @@ import java.util.stream.Collectors class MeasurementMeasurementProteomicsValidatorSpec extends Specification { final static ProteomicsMeasurementMetadata validMetadata = new ProteomicsMeasurementMetadata("", SampleCode.create("QTEST001AE"), + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -40,14 +41,14 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { "http://www.ebi.ac.uk/efo/EFO_0004205" ) final TerminologyService terminologyService = Mock(TerminologyService.class, { - findByCurie(validMetadata.instrumentCURI()) >> Optional.of(illuminaMiSeq) + findByCurie(validMetadata.msDeviceCURIE()) >> Optional.of(illuminaMiSeq) }) final MeasurementService measurementService = Mock(MeasurementService.class) final ProjectInformationService projectInformationService = Mock(ProjectInformationService.class) - final static List validPXPProperties = Collections.unmodifiableList(["qbic sample id", "sample name", "organisation id", "facility", "instrument", + final static List validPXPProperties = Collections.unmodifiableList(["qbic sample id", "sample name", "technical replicate", "organisation id", "facility", "ms device", "sample pool group", "cycle/fraction name", "digestion method", "digestion enzyme", "enrichment method", "injection volume (uL)", "lc column", "lcms method", "labeling type", "label", "comment"]) @@ -131,6 +132,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { def "An unknown sample code in a proteomics measurement metadata object must return a failed validation "() { given: def invalidMeasurementEntry = new ProteomicsMeasurementMetadata("", SampleCode.create("QNKWN001AE"), + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -168,6 +170,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { def "If no sample code is provided, the validation must fail"() { given: def invalidMeasurementEntry = new ProteomicsMeasurementMetadata("", null, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -210,6 +213,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", invalidRorId, //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -257,6 +261,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "", // missing entry "EFO:0004205", //Illumina MiSeq "1", @@ -296,6 +301,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata validMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", validRorId, //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -336,10 +342,11 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { } - def "If no instrument Curie for the instrument information is provided, the validation must fail"() { + def "If no MS device Curie for the MS device information is provided, the validation must fail"() { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "", //Illumina MiSeq "1", @@ -372,15 +379,16 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { !result.containsWarnings() result.containsFailures() result.failedEntries() == 1 - result.failures()[0] == "Instrument: missing mandatory metadata" + result.failures()[0] == "MS Device: missing mandatory metadata" } - def "If a valid instrument curie for the instrument information is provided, the validation must pass"() { + def "If a valid ms device curie for the ms device information is provided, the validation must pass"() { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, - validInstrumentCurie, //Illumina MiSeq + validMsDeviceCurie, //Illumina MiSeq "1", "The geniuses of ITSS", "4 Nations lived in harmony", @@ -412,7 +420,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { !result.containsFailures() where: - validInstrumentCurie << [ + validMsDeviceCurie << [ "EFO:0004205", // Illumina MiSeq ] } @@ -422,6 +430,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -461,6 +470,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -498,6 +508,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -537,6 +548,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -576,6 +588,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", @@ -614,6 +627,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { given: SampleCode validSampleCode = SampleCode.create("QTEST001AE") ProteomicsMeasurementMetadata invalidMetadata = new ProteomicsMeasurementMetadata("", validSampleCode, + "", "https://ror.org/03a1kwz48", //Universität Tübingen, "EFO:0004205", //Illumina MiSeq "1", diff --git a/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java b/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java index d69933cd56..2bda75faf2 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java +++ b/user-interface/src/main/java/life/qbic/datamanager/parser/MetadataConverter.java @@ -1,21 +1,5 @@ package life.qbic.datamanager.parser; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.COMMENT; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.CYCLE; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.DIGESTION_ENZYME; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.DIGESTION_METHOD; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.ENRICHMENT_METHOD; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.FACILITY; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.INJECTION_VOLUME; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.INSTRUMENT; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.LABEL; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.LABELING_TYPE; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.LCMS_METHOD; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.LC_COLUMN; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.MEASUREMENT_ID; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.ORGANISATION_ID; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.QBIC_SAMPLE_ID; -import static life.qbic.datamanager.parser.MetadataConverter.ProteomicsMeasurementProperty.SAMPLE_POOL_GROUP; import static life.qbic.logging.service.LoggerFactory.logger; import java.util.ArrayList; @@ -132,37 +116,66 @@ private List convertProteomicsMeasurement(ParsingResult par for (ParsingResult.Row row : parsingResult.rows()) { // we us -1 as default value if a property cannot be accessed, thus ending up in an empty String var pxpMetaDatum = new ProteomicsMeasurementMetadata( - safeListAccess(row.values(), keyIndices.getOrDefault(MEASUREMENT_ID.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.MEASUREMENT_ID.propertyName(), + -1), ""), SampleCode.create( safeListAccess(row.values(), - keyIndices.getOrDefault(QBIC_SAMPLE_ID.propertyName(), -1), + keyIndices.getOrDefault( + ProteomicsMeasurementProperty.QBIC_SAMPLE_ID.propertyName(), -1), "")), - safeListAccess(row.values(), keyIndices.getOrDefault(ORGANISATION_ID.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault( + ProteomicsMeasurementProperty.TECHNICAL_REPLICATE_NAME.propertyName(), -1), ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.ORGANISATION_ID.propertyName(), + -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(INSTRUMENT.propertyName(), -1), ""), safeListAccess(row.values(), - keyIndices.getOrDefault(SAMPLE_POOL_GROUP.propertyName(), -1), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.MS_DEVICE.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(FACILITY.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(CYCLE.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(DIGESTION_ENZYME.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault( + ProteomicsMeasurementProperty.SAMPLE_POOL_GROUP.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(DIGESTION_METHOD.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.FACILITY.propertyName(), -1), ""), safeListAccess(row.values(), - keyIndices.getOrDefault(ENRICHMENT_METHOD.propertyName(), -1), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.CYCLE.propertyName(), -1), ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.DIGESTION_ENZYME.propertyName(), + -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(INJECTION_VOLUME.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.DIGESTION_METHOD.propertyName(), + -1), + ""), + safeListAccess(row.values(), + keyIndices.getOrDefault( + ProteomicsMeasurementProperty.ENRICHMENT_METHOD.propertyName(), -1), + ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.INJECTION_VOLUME.propertyName(), + -1), + ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.LC_COLUMN.propertyName(), -1), + ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.LCMS_METHOD.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(LC_COLUMN.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(LCMS_METHOD.propertyName(), -1), ""), new Labeling( safeListAccess(row.values(), - keyIndices.getOrDefault(LABELING_TYPE.propertyName(), -1), + keyIndices.getOrDefault( + ProteomicsMeasurementProperty.LABELING_TYPE.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(LABEL.propertyName(), -1), "")), - safeListAccess(row.values(), keyIndices.getOrDefault(COMMENT.propertyName(), -1), "") + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.LABEL.propertyName(), -1), + "")), + safeListAccess(row.values(), + keyIndices.getOrDefault(ProteomicsMeasurementProperty.COMMENT.propertyName(), -1), "") ); result.add(pxpMetaDatum); } @@ -181,16 +194,20 @@ private List convertNGSMeasurement(ParsingResult parsingRes var keyIndices = parsingResult.keys(); for (Row row : parsingResult.rows()) { var ngsMeasurementMetadata = new NGSMeasurementMetadata( - safeListAccess(row.values(), keyIndices.getOrDefault(MEASUREMENT_ID.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault(NGSMeasurementProperty.MEASUREMENT_ID.propertyName(), -1), ""), List.of(SampleCode.create( safeListAccess(row.values(), - keyIndices.getOrDefault(QBIC_SAMPLE_ID.propertyName(), -1), + keyIndices.getOrDefault(NGSMeasurementProperty.QBIC_SAMPLE_ID.propertyName(), -1), ""))), - safeListAccess(row.values(), keyIndices.getOrDefault(ORGANISATION_ID.propertyName(), -1), + safeListAccess(row.values(), + keyIndices.getOrDefault(NGSMeasurementProperty.ORGANISATION_ID.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(INSTRUMENT.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(FACILITY.propertyName(), -1), ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(NGSMeasurementProperty.INSTRUMENT.propertyName(), -1), ""), + safeListAccess(row.values(), + keyIndices.getOrDefault(NGSMeasurementProperty.FACILITY.propertyName(), -1), ""), safeListAccess(row.values(), keyIndices.getOrDefault( NGSMeasurementProperty.SEQUENCING_READ_TYPE.propertyName(), -1), ""), safeListAccess(row.values(), @@ -202,7 +219,7 @@ private List convertNGSMeasurement(ParsingResult parsingRes NGSMeasurementProperty.SEQUENCING_RUN_PROTOCOL.propertyName(), -1), ""), safeListAccess(row.values(), - keyIndices.getOrDefault(SAMPLE_POOL_GROUP.propertyName(), -1), + keyIndices.getOrDefault(NGSMeasurementProperty.SAMPLE_POOL_GROUP.propertyName(), -1), ""), safeListAccess(row.values(), keyIndices.getOrDefault(NGSMeasurementProperty.INDEX_I7.propertyName(), -1), @@ -210,7 +227,8 @@ private List convertNGSMeasurement(ParsingResult parsingRes safeListAccess(row.values(), keyIndices.getOrDefault(NGSMeasurementProperty.INDEX_I5.propertyName(), -1), ""), - safeListAccess(row.values(), keyIndices.getOrDefault(COMMENT.propertyName(), -1), "") + safeListAccess(row.values(), + keyIndices.getOrDefault(NGSMeasurementProperty.COMMENT.propertyName(), -1), "") ); result.add(ngsMeasurementMetadata); } @@ -222,11 +240,11 @@ private boolean looksLikeNgsMeasurement(Collection properties, boolean i .collect(Collectors.toList()); Map hitMap; if (ignoreID) { - formattedProperties.remove(MEASUREMENT_ID.propertyName()); + formattedProperties.remove(NGSMeasurementProperty.MEASUREMENT_ID.propertyName()); hitMap = countHits(formattedProperties, Arrays.stream(NGSMeasurementProperty.values()) .map(NGSMeasurementProperty::propertyName).collect( - Collectors.toSet()), MEASUREMENT_ID.propertyName()); + Collectors.toSet()), NGSMeasurementProperty.MEASUREMENT_ID.propertyName()); } else { hitMap = countHits(formattedProperties, Arrays.stream(NGSMeasurementProperty.values()) @@ -252,11 +270,11 @@ private boolean looksLikeProteomicsMeasurement(Collection properties, bo .collect(Collectors.toList()); Map hitMap; if (ignoreID) { - formattedProperties.remove(MEASUREMENT_ID.propertyName()); + formattedProperties.remove(ProteomicsMeasurementProperty.MEASUREMENT_ID.propertyName()); hitMap = countHits(formattedProperties, Arrays.stream(ProteomicsMeasurementProperty.values()) .map(ProteomicsMeasurementProperty::propertyName).collect( - Collectors.toSet()), MEASUREMENT_ID.propertyName()); + Collectors.toSet()), ProteomicsMeasurementProperty.MEASUREMENT_ID.propertyName()); } else { hitMap = countHits(formattedProperties, Arrays.stream(ProteomicsMeasurementProperty.values()) @@ -280,16 +298,17 @@ private boolean looksLikeProteomicsMeasurement(Collection properties, bo enum ProteomicsMeasurementProperty { MEASUREMENT_ID("measurement id"), + TECHNICAL_REPLICATE_NAME("technical replicate"), QBIC_SAMPLE_ID("qbic sample id"), SAMPLE_POOL_GROUP("sample pool group"), ORGANISATION_ID("organisation id"), FACILITY("facility"), - INSTRUMENT("instrument"), + MS_DEVICE("ms device"), CYCLE("cycle/fraction name"), DIGESTION_METHOD("digestion method"), DIGESTION_ENZYME("digestion enzyme"), ENRICHMENT_METHOD("enrichment method"), - INJECTION_VOLUME("injection volume (ul)"), + INJECTION_VOLUME("injection volume (µl)"), LC_COLUMN("lc column"), LCMS_METHOD("lcms method"), LABELING_TYPE("labeling type"), @@ -323,6 +342,7 @@ public String propertyName() { enum NGSMeasurementProperty { MEASUREMENT_ID("measurement id"), + QBIC_SAMPLE_ID("qbic sample id"), ORGANISATION_ID("organisation id"), SAMPLE_POOL_GROUP("sample pool group"), FACILITY("facility"), diff --git a/user-interface/src/main/java/life/qbic/datamanager/spreadsheet/XLSXTemplateHelper.java b/user-interface/src/main/java/life/qbic/datamanager/spreadsheet/XLSXTemplateHelper.java new file mode 100644 index 0000000000..1ddec66fe6 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/spreadsheet/XLSXTemplateHelper.java @@ -0,0 +1,205 @@ +package life.qbic.datamanager.spreadsheet; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +import java.util.List; +import java.util.Random; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.SheetVisibility; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.ss.util.CellReference; + +/** + * Helps to create excel template sheets. + *

+ * This class provides methods to set up validation, lock and hide sheets and a safe way to access + * rows and cells in a sheet. + * + * @since 1.5.0 + */ +public class XLSXTemplateHelper { + + private static final Random RANDOM = new Random(); + + protected XLSXTemplateHelper() { + //hide constructor as static methods only are used + } + + /** + * Asks for a specific row with an index starting with index 0 for the first row. If no row + * exists, creates a new and empty row. + * + * @param sheet the sheet to ask for the row + * @param rowIndex the index of the row starting with 0 for the first row. + * @return the row in the sheet + * @since 1.5.0 + */ + public static Row getOrCreateRow(Sheet sheet, int rowIndex) { + Row row = sheet.getRow(rowIndex); + if (isNull(row)) { + row = sheet.createRow(rowIndex); + } + return row; + } + + /** + * Asks for a specific cell with a column index starting with index 0 for the first column. If no + * cell exists, creates a new and blank cell. + * + * @param row the row to ask for the cell + * @param cellIndex the column index of the cell in the row. Starting with 0 for the first + * column. + * @return the cell in the row + * @since 1.5.0 + */ + public static Cell getOrCreateCell(Row row, int cellIndex) { + Cell cell = row.getCell(cellIndex); + if (nonNull(cell)) { + return cell; + } + return row.createCell(cellIndex); + } + + /** + * The value within the cell as String + * + * @param cell the cell to read the value from, must not be null. + * @return the value of the cell. Never null. + * @since 1.5.0 + */ + public static String getCellValueAsString(Cell cell) { + if (isNull(cell)) { + throw new IllegalArgumentException("cell is null"); + } + return switch (cell.getCellType()) { + case FORMULA, _NONE, BLANK, ERROR -> ""; + case STRING -> cell.getStringCellValue(); + case BOOLEAN -> Boolean.toString(cell.getBooleanCellValue()); + case NUMERIC -> Double.toString(cell.getNumericCellValue()); + }; + } + + /** + * Adds values to the sheet and returns the named area where they were added. + * + * @param sheet the sheet where to add the values + * @param propertyName the name of the property + * @param options the available options to choose a value from + * @return a defined name for a range of cells in the workbook. + * @see Name + * @since 1.5.0 + */ + public static Name createOptionArea(Sheet sheet, String propertyName, + List options) { + Row headerRow = getOrCreateRow(sheet, 0); + var columnNumber = Math.max(1, + headerRow.getLastCellNum()); // we want to obtain 1 for the first to come if there are none and not -1 -.- + var columnIndex = columnNumber - 1; + + // create header cell + Cell headerRowCell = headerRow.createCell(columnIndex); + headerRowCell.setCellValue(propertyName); + + var startIndex = 1; // ignore the header at 0 + var rowIndex = startIndex; + for (String option : options) { + Row valueRow = getOrCreateRow(sheet, rowIndex); + getOrCreateCell(valueRow, columnIndex) + .setCellValue(option); + rowIndex++; + } + var reference = "'%s'!$%s$%s:$%s$%s".formatted( //e.g. 'My Sheet'!$A$2:$E$23 + sheet.getSheetName(), + CellReference.convertNumToColString(columnIndex), + 1 + startIndex, //shift by start index + CellReference.convertNumToColString(columnIndex), + options.size() + startIndex //shift by start index + ); + var namedArea = sheet.getWorkbook().createName(); + + namedArea.setNameName(toCamelCase(propertyName)); + namedArea.setRefersToFormula(reference); + return namedArea; + } + + /** + * Converts a string to camel case. Leaves the first character as is. Considers non-word + * characters as well as underscores to be word separators. + *

+ * For example: "this is a sentence" and "this_is_a_string" become "thisIsASentence" + * + * @param input the input to camel case + * @return a camel case representation of the input + * @since 1.5.0 + */ + protected static String toCamelCase(String input) { + StringBuilder stringBuilder = new StringBuilder(input); + Predicate isWordSeparator = character -> String.valueOf(character).matches("\\W|_"); + for (int i = 0; i < stringBuilder.length(); i++) { + if (isWordSeparator.test(stringBuilder.charAt(i))) { + stringBuilder.deleteCharAt( + i); //remove the separator shifting the next character into position i + if (stringBuilder.length() <= i) { + //the last character was removed + break; + } + stringBuilder.replace(i, i + 1, + String.valueOf(stringBuilder.charAt(i)).toUpperCase());//capitalize next character + } + } + return stringBuilder.toString(); + } + + /** + * Adds data validation to an area in the spreadsheet. Requires the valid options to be set + * beforehand as a name. This can be done by using {@link #createOptionArea(Sheet, String, List)} + * + * @param sheet the sheet in which the validation should be added + * @param startColIdx the start column of the validated values >= 0 + * @param startRowIdx the start row of the validated values >= 0 + * @param stopColIdx the last column of the validated values >= startColIdx + * @param stopRowIdx the last row of the validated values >= startRowIdx + * @param allowedValues the named area defining the allowed values + * @since 1.5.0 + */ + public static void addDataValidation(Sheet sheet, int startColIdx, int startRowIdx, + int stopColIdx, int stopRowIdx, Name allowedValues) { + CellRangeAddressList validatedCells = new CellRangeAddressList(startRowIdx, + stopRowIdx, + startColIdx, + stopColIdx); + DataValidationHelper dataValidationHelper = sheet.getDataValidationHelper(); + DataValidationConstraint formulaListConstraint = dataValidationHelper + .createFormulaListConstraint(allowedValues.getNameName()); + DataValidation validation = dataValidationHelper.createValidation(formulaListConstraint, + validatedCells); + validation.setSuppressDropDownArrow(true); // shows dropdown if true + validation.setShowErrorBox(true); + validation.createErrorBox("Invalid choice", "Please select a value from the dropdown list."); + sheet.addValidationData(validation); + } + + + public static void hideSheet(Workbook workbook, Sheet sheet) { + workbook.setSheetVisibility(workbook.getSheetIndex(sheet), SheetVisibility.VERY_HIDDEN); + } + + public static void lockSheet(Sheet sheet) { + String randomPassword = RANDOM.ints(16) + .mapToObj(Integer::toString) + .collect(Collectors.joining()); + sheet.protectSheet(randomPassword); + } + + +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java index ecc86b5299..3814a98e9a 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java @@ -302,10 +302,10 @@ private void createProteomicsGrid() { .setAutoWidth(true); proteomicsMeasurementGrid.addComponentColumn( proteomicsMeasurement -> renderInstrument().createComponent( - proteomicsMeasurement.instrument())) - .setHeader("Instrument") + proteomicsMeasurement.msDevice())) + .setHeader("MS Device") .setTooltipGenerator( - proteomicsMeasurement -> proteomicsMeasurement.instrument().formatted()) + proteomicsMeasurement -> proteomicsMeasurement.msDevice().formatted()) .setAutoWidth(true); proteomicsMeasurementGrid.addColumn(ProteomicsMeasurement::digestionEnzyme) .setHeader("Digestion Enzyme").setTooltipGenerator( diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementPresenter.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementPresenter.java index 672c183ad1..15b33bd1ab 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementPresenter.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementPresenter.java @@ -34,15 +34,24 @@ private static ProteomicsMeasurementEntry convertProteomicsMeasurement( SampleInformation sampleInfo, ProteomicsSpecificMeasurementMetadata specificMeasurementMetadata) { return new ProteomicsMeasurementEntry(measurement.measurementCode().value(), - sampleInfo, measurement.organisation().IRI(), measurement.organisation().label(), - measurement.instrument().getOboId().replace("_", ":"), - measurement.instrument().getLabel(), - measurement.samplePoolGroup().orElse(""), measurement.facility(), + sampleInfo, + measurement.technicalReplicateName().orElse(""), + measurement.organisation().IRI(), + measurement.organisation().label(), + measurement.msDevice().getOboId().replace("_", ":"), + measurement.msDevice().getLabel(), + measurement.samplePoolGroup().orElse(""), + measurement.facility(), specificMeasurementMetadata.fractionName(), - measurement.digestionEnzyme(), measurement.digestionMethod(), - measurement.enrichmentMethod(), String.valueOf(measurement.injectionVolume()), - measurement.lcColumn(), measurement.lcmsMethod(), measurement.labelType(), - specificMeasurementMetadata.label(), ""); + measurement.digestionEnzyme(), + measurement.digestionMethod(), + measurement.enrichmentMethod(), + String.valueOf(measurement.injectionVolume()), + measurement.lcColumn(), + measurement.lcmsMethod(), + measurement.labelType(), + specificMeasurementMetadata.label(), + ""); } private static NGSMeasurementEntry convertNGSMeasurement(NGSMeasurement measurement, diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/ProteomicsMeasurementEntry.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/ProteomicsMeasurementEntry.java index 4c8aa5ac26..752e1fe9a7 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/ProteomicsMeasurementEntry.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/ProteomicsMeasurementEntry.java @@ -9,10 +9,11 @@ */ public record ProteomicsMeasurementEntry(String measurementCode, SampleInformation sampleInformation, + String technicalReplicateName, String organisationId, String organisationName, - String instrumentCURI, - String instrumentName, + String msDeviceCURIE, + String msDeviceName, String samplePoolGroup, String facility, String fractionName, @@ -25,6 +26,4 @@ public record ProteomicsMeasurementEntry(String measurementCode, String labelingType, String label, String comment) { - - } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java index 05d96a4ae4..6254294d5d 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java @@ -1,20 +1,30 @@ package life.qbic.datamanager.views.projects.project.measurements.download; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.createOptionArea; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.getOrCreateCell; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.getOrCreateRow; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.hideSheet; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.lockSheet; import static life.qbic.logging.service.LoggerFactory.logger; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.ApplicationException.ErrorCode; +import life.qbic.datamanager.spreadsheet.XLSXTemplateHelper; import life.qbic.datamanager.views.general.download.DownloadContentProvider; import life.qbic.datamanager.views.projects.project.measurements.NGSMeasurementEntry; import life.qbic.logging.api.Logger; +import life.qbic.projectmanagement.application.measurement.NGSMeasurementMetadata; +import life.qbic.projectmanagement.domain.model.measurement.NGSMeasurement; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -23,11 +33,11 @@ import org.apache.poi.xssf.usermodel.XSSFFont; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -/** NGS Measurement Content Provider +/** + * NGS Measurement Content Provider *

- * Implementation of the {@link DownloadContentProvider} providing the content and file name for any files created - * from {@link life.qbic.projectmanagement.domain.model.measurement.NGSMeasurement} - * and {@link life.qbic.projectmanagement.application.measurement.NGSMeasurementMetadata} + * Implementation of the {@link DownloadContentProvider} providing the content and file name for any + * files created from {@link NGSMeasurement} and {@link NGSMeasurementMetadata} *

*/ public class NGSMeasurementContentProvider implements DownloadContentProvider { @@ -41,7 +51,22 @@ public class NGSMeasurementContentProvider implements DownloadContentProvider { private static CellStyle boldStyle; private final List measurements = new LinkedList<>(); private static final String DEFAULT_FILE_NAME_PREFIX = "QBiC"; - private String fileNamePrefix = DEFAULT_FILE_NAME_PREFIX ; + private String fileNamePrefix = DEFAULT_FILE_NAME_PREFIX; + private static final int DEFAULT_GENERATED_ROW_COUNT = 200; + + private enum SequencingReadType { + SINGLE_END("single-end"), + PAIRED_END("paired-end"); + private final String presentationString; + + SequencingReadType(String presentationString) { + this.presentationString = presentationString; + } + + static List getOptions() { + return Arrays.stream(values()).map(it -> it.presentationString).toList(); + } + } private static void setAutoWidth(Sheet sheet) { for (int col = 0; col <= NGSMeasurementColumns.values().length; col++) { @@ -51,7 +76,7 @@ private static void setAutoWidth(Sheet sheet) { private static void formatHeader(Row header) { for (NGSMeasurementColumns value : NGSMeasurementColumns.values()) { - var cell = header.createCell(value.columnNumber()); + var cell = header.createCell(value.columnIndex()); cell.setCellValue(value.headerName()); setHeaderStyle(cell, value.readOnly()); } @@ -71,70 +96,36 @@ private static void setCellStyle(Cell cell, boolean isReadOnly) { } } - private static void createMeasurementEntry(NGSMeasurementEntry ngsMeasurementEntry, Row entry) { - var measureCol = entry.createCell(NGSMeasurementColumns.MEASUREMENTCODE.columnNumber()); - measureCol.setCellValue(ngsMeasurementEntry.measurementCode()); - setCellStyle(measureCol, NGSMeasurementColumns.MEASUREMENTCODE.readOnly()); - - var sampleIdCol = entry.createCell(NGSMeasurementColumns.SAMPLEID.columnNumber()); - sampleIdCol.setCellValue(ngsMeasurementEntry.sampleInformation().sampleId()); - setCellStyle(sampleIdCol, NGSMeasurementColumns.SAMPLEID.readOnly()); - - var sampleNameCol = entry.createCell(NGSMeasurementColumns.SAMPLENAME.columnNumber()); - sampleNameCol.setCellValue(ngsMeasurementEntry.sampleInformation().sampleName()); - setCellStyle(sampleNameCol, NGSMeasurementColumns.SAMPLENAME.readOnly()); - - var orgIdCol = entry.createCell(NGSMeasurementColumns.ORGANISATIONID.columnNumber()); - orgIdCol.setCellValue(ngsMeasurementEntry.organisationId()); - setCellStyle(orgIdCol, NGSMeasurementColumns.ORGANISATIONID.readOnly()); - - var organisationNameCol = entry.createCell( - NGSMeasurementColumns.ORGANISATIONNAME.columnNumber()); - organisationNameCol.setCellValue(ngsMeasurementEntry.organisationName()); - setCellStyle(organisationNameCol, NGSMeasurementColumns.ORGANISATIONNAME.readOnly()); - - var facilityCol = entry.createCell(NGSMeasurementColumns.FACILITY.columnNumber()); - facilityCol.setCellValue(ngsMeasurementEntry.facility()); - setCellStyle(facilityCol, NGSMeasurementColumns.FACILITY.readOnly); - var instrumentCol = entry.createCell(NGSMeasurementColumns.INSTRUMENT.columnNumber()); - instrumentCol.setCellValue(ngsMeasurementEntry.instrumentCURI()); - setCellStyle(instrumentCol, NGSMeasurementColumns.INSTRUMENT.readOnly()); - - var instrumentNameCol = entry.createCell(NGSMeasurementColumns.INSTRUMENTNAME.columnNumber()); - instrumentNameCol.setCellValue(ngsMeasurementEntry.instrumentName()); - setCellStyle(instrumentNameCol, NGSMeasurementColumns.INSTRUMENTNAME.readOnly()); - - var readTypeCol = entry.createCell(NGSMeasurementColumns.SEQUENCINGREADTYPE.columnNumber()); - readTypeCol.setCellValue(ngsMeasurementEntry.readType()); - setCellStyle(readTypeCol, NGSMeasurementColumns.SEQUENCINGREADTYPE.readOnly()); - - var libraryKitCol = entry.createCell(NGSMeasurementColumns.LIBRARYKIT.columnNumber()); - libraryKitCol.setCellValue(ngsMeasurementEntry.libraryKit()); - setCellStyle(libraryKitCol, NGSMeasurementColumns.LIBRARYKIT.readOnly()); - - var flowCellCol = entry.createCell(NGSMeasurementColumns.FLOWCELL.columnNumber()); - flowCellCol.setCellValue(ngsMeasurementEntry.flowCell()); - setCellStyle(flowCellCol, NGSMeasurementColumns.FLOWCELL.readOnly()); - - var runProtocolCol = entry.createCell(NGSMeasurementColumns.RUNPROTOCOL.columnNumber()); - runProtocolCol.setCellValue(ngsMeasurementEntry.runProtocol()); - setCellStyle(runProtocolCol, NGSMeasurementColumns.RUNPROTOCOL.readOnly()); - - var poolGroupCol = entry.createCell(NGSMeasurementColumns.POOLGROUP.columnNumber()); - poolGroupCol.setCellValue(ngsMeasurementEntry.samplePoolGroup()); - setCellStyle(poolGroupCol, NGSMeasurementColumns.POOLGROUP.readOnly()); - - var indexI7Col = entry.createCell(NGSMeasurementColumns.INDEXI7.columnNumber()); - indexI7Col.setCellValue(ngsMeasurementEntry.indexI7()); - setCellStyle(indexI7Col, NGSMeasurementColumns.INDEXI7.readOnly()); - - var indexI5Col = entry.createCell(NGSMeasurementColumns.INDEXI5.columnNumber()); - indexI5Col.setCellValue(ngsMeasurementEntry.indexI5()); - setCellStyle(indexI5Col, NGSMeasurementColumns.INDEXI5.readOnly()); - - var commentCol = entry.createCell(NGSMeasurementColumns.COMMENT.columnNumber()); - commentCol.setCellValue(ngsMeasurementEntry.comment()); - setCellStyle(commentCol, NGSMeasurementColumns.COMMENT.readOnly()); + private static void writeMeasurementIntoRow(NGSMeasurementEntry ngsMeasurementEntry, + Row entryRow) { + + for (NGSMeasurementColumns measurementColumn : NGSMeasurementColumns.values()) { + var value = switch (measurementColumn) { + case MEASUREMENT_ID -> ngsMeasurementEntry.measurementCode(); + case SAMPLE_ID -> ngsMeasurementEntry.sampleInformation().sampleId(); + case SAMPLE_NAME -> ngsMeasurementEntry.sampleInformation().sampleName(); + case POOL_GROUP -> ngsMeasurementEntry.samplePoolGroup(); + case ORGANISATION_ID -> ngsMeasurementEntry.organisationId(); + case ORGANISATION_NAME -> ngsMeasurementEntry.organisationName(); + case FACILITY -> ngsMeasurementEntry.facility(); + case INSTRUMENT -> ngsMeasurementEntry.instrumentCURI(); + case INSTRUMENT_NAME -> ngsMeasurementEntry.instrumentName(); + case SEQUENCING_READ_TYPE -> ngsMeasurementEntry.readType(); + case LIBRARY_KIT -> ngsMeasurementEntry.libraryKit(); + case FLOW_CELL -> ngsMeasurementEntry.flowCell(); + case SEQUENCING_RUN_PROTOCOL -> ngsMeasurementEntry.runProtocol(); + case INDEX_I7 -> ngsMeasurementEntry.indexI7(); + case INDEX_I5 -> ngsMeasurementEntry.indexI5(); + case COMMENT -> ngsMeasurementEntry.comment(); + }; + var cell = getOrCreateCell(entryRow, measurementColumn.columnIndex()); + cell.setCellValue(value); + if (measurementColumn.readOnly()) { + cell.setCellStyle(readOnlyCellStyle); + } + } + + } public void setMeasurements(List measurements, String fileNamePrefix) { @@ -171,6 +162,7 @@ private void defineReadOnlyHeaderStyle(Workbook workbook) { readOnlyHeaderStyle.setFont(fontHeader); } + @Override public byte[] getContent() { if (measurements.isEmpty()) { @@ -180,23 +172,47 @@ public byte[] getContent() { ByteArrayOutputStream byteArrayOutputStream; try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("NGS Measurement Metadata"); - - Row header = sheet.createRow(0); defineReadOnlyHeaderStyle(workbook); defineReadOnlyCellStyle(workbook); defineBoldStyle(workbook); - formatHeader(header); - int rowCounter = 1; + Sheet sheet = workbook.createSheet("NGS Measurement Metadata"); + + Row header = getOrCreateRow(sheet, 0); + for (NGSMeasurementColumns value : NGSMeasurementColumns.values()) { + var cell = getOrCreateCell(header, value.columnIndex()); + cell.setCellValue(value.headerName()); + setHeaderStyle(cell, value.readOnly()); + } - for (NGSMeasurementEntry ngsMeasurementEntry : measurements) { - Row entry = sheet.createRow(rowCounter); - createMeasurementEntry(ngsMeasurementEntry, entry); - rowCounter++; + var startIndex = 1; // start in row number 2 with index 1 as the header row has number 1 index 0 + int rowIndex = startIndex; + for (NGSMeasurementEntry measurement : measurements) { + Row row = getOrCreateRow(sheet, rowIndex); + writeMeasurementIntoRow(measurement, row); + rowIndex++; } + var generatedRowCount = rowIndex - startIndex; + assert generatedRowCount == measurements.size() : "all measurements have a corresponding row"; + + // make sure to create the visible sheet first + Sheet hiddenSheet = workbook.createSheet("hidden"); + Name sequencingReadTypeArea = createOptionArea(hiddenSheet, + "Sequencing read type", SequencingReadType.getOptions()); + + XLSXTemplateHelper.addDataValidation(sheet, + NGSMeasurementColumns.SEQUENCING_READ_TYPE.columnIndex(), + startIndex, + NGSMeasurementColumns.SEQUENCING_READ_TYPE.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + sequencingReadTypeArea); + setAutoWidth(sheet); + workbook.setActiveSheet(0); + + lockSheet(hiddenSheet); + hideSheet(workbook, hiddenSheet); byteArrayOutputStream = new ByteArrayOutputStream(); workbook.write(byteArrayOutputStream); @@ -210,60 +226,73 @@ public byte[] getContent() { @Override public String getFileName() { - return String.join("_" , fileNamePrefix, FILE_NAME_SUFFIX); + return String.join("_", fileNamePrefix, FILE_NAME_SUFFIX); } /** * NGS Measurement Columns * - *

Enumeration of the columns shown in the file used for NGS measurement registration and edit in the context of measurement file based upload. - * Provides the name of the header column, the column index and if the column should be set to readOnly in the generated sheet + *

Enumeration of the columns shown in the file used for NGS measurement registration and edit + * in the context of measurement file based upload. + * Provides the name of the header column, the column index and if the column should be set to + * readOnly in the generated sheet *

*/ enum NGSMeasurementColumns { - MEASUREMENTCODE("Measurement ID", 0, + MEASUREMENT_ID("Measurement ID", 0, true), - SAMPLEID("QBiC Sample Id", 1, + SAMPLE_ID("QBiC Sample Id", 1, true), - SAMPLENAME( + SAMPLE_NAME( "Sample Name", 2, true), - POOLGROUP("Sample Pool Group", 3, + POOL_GROUP("Sample Pool Group", 3, true), - ORGANISATIONID("Organisation ID", 4, + ORGANISATION_ID("Organisation ID", 4, false), - ORGANISATIONNAME("Organisation Name", 5, + ORGANISATION_NAME("Organisation Name", 5, true), FACILITY("Facility", 6, false), INSTRUMENT("Instrument", 7, false), - INSTRUMENTNAME("Instrument Name", 8, + INSTRUMENT_NAME("Instrument Name", 8, true), - SEQUENCINGREADTYPE("Sequencing Read Type", 9, + SEQUENCING_READ_TYPE("Sequencing Read Type", 9, false), - LIBRARYKIT("Library Kit", 10, + LIBRARY_KIT("Library Kit", 10, false), - FLOWCELL("Flow Cell", 11, + FLOW_CELL("Flow Cell", 11, false), - RUNPROTOCOL("Sequencing Run Protocol", 12, + SEQUENCING_RUN_PROTOCOL("Sequencing Run Protocol", 12, false), - INDEXI7("Index i7", 13, + INDEX_I7("Index i7", 13, false), - INDEXI5("Index i5", 14, + INDEX_I5("Index i5", 14, false), COMMENT("Comment", 15, - false); + false), + ; private final String headerName; - private final int columnNumber; + private final int columnIndex; private final boolean readOnly; + static int maxColumnIndex() { + return Arrays.stream(values()) + .mapToInt(NGSMeasurementColumns::columnIndex) + .max().orElse(0); + } - NGSMeasurementColumns(String headerName, int columnNumber, boolean readOnly) { + /** + * @param headerName the name in the header + * @param columnIndex the index of the column this property is in + * @param readOnly is the property read only + */ + NGSMeasurementColumns(String headerName, int columnIndex, boolean readOnly) { this.headerName = headerName; - this.columnNumber = columnNumber; + this.columnIndex = columnIndex; this.readOnly = readOnly; } @@ -271,12 +300,13 @@ public String headerName() { return headerName; } - public int columnNumber() { - return columnNumber; + public int columnIndex() { + return columnIndex; } public boolean readOnly() { return readOnly; } + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java index 2850238307..05de7d2b43 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java @@ -1,5 +1,11 @@ package life.qbic.datamanager.views.projects.project.measurements.download; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.addDataValidation; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.createOptionArea; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.getOrCreateCell; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.getOrCreateRow; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.hideSheet; +import static life.qbic.datamanager.spreadsheet.XLSXTemplateHelper.lockSheet; import static life.qbic.logging.service.LoggerFactory.logger; import java.io.ByteArrayOutputStream; @@ -11,9 +17,13 @@ import life.qbic.datamanager.views.general.download.DownloadContentProvider; import life.qbic.datamanager.views.projects.project.measurements.ProteomicsMeasurementEntry; import life.qbic.logging.api.Logger; +import life.qbic.projectmanagement.application.measurement.ProteomicsMeasurementMetadata; +import life.qbic.projectmanagement.application.measurement.validation.MeasurementProteomicsValidator.DigestionMethod; +import life.qbic.projectmanagement.domain.model.measurement.ProteomicsMeasurement; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Name; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -25,8 +35,8 @@ /** Proteomics Measurement Content Provider *

* Implementation of the {@link DownloadContentProvider} providing the content and file name for any files created - * from {@link life.qbic.projectmanagement.domain.model.measurement.ProteomicsMeasurement} - * and {@link life.qbic.projectmanagement.application.measurement.ProteomicsMeasurementMetadata} + * from {@link ProteomicsMeasurement} + * and {@link ProteomicsMeasurementMetadata} *

*/ public class ProteomicsMeasurementContentProvider implements DownloadContentProvider { @@ -38,6 +48,8 @@ public class ProteomicsMeasurementContentProvider implements DownloadContentProv private final List measurements = new LinkedList<>(); private static final String DEFAULT_FILE_NAME_PREFIX = "QBiC"; private String fileNamePrefix = DEFAULT_FILE_NAME_PREFIX; + private static final int DEFAULT_GENERATED_ROW_COUNT = 200; + private static void setAutoWidth(Sheet sheet) { for (int col = 0; col <= 18; col++) { @@ -45,100 +57,37 @@ private static void setAutoWidth(Sheet sheet) { } } - private static void formatHeader(Row header, CellStyle readOnlyHeader, CellStyle boldStyle) { - var h1 = header.createCell(0); - h1.setCellValue("Measurement ID"); - h1.setCellStyle(readOnlyHeader); - - var h2 = header.createCell(1); - h2.setCellValue("QBiC Sample ID"); - h2.setCellStyle(readOnlyHeader); - - var h3 = header.createCell(2); - h3.setCellValue("Sample Name"); - h3.setCellStyle(readOnlyHeader); - - var h4 = header.createCell(3); - h4.setCellValue("Sample Pool Group"); - h4.setCellStyle(readOnlyHeader); - - var h5 = header.createCell(4); - h5.setCellValue("Organisation ID"); - h5.setCellStyle(boldStyle); - - var h6 = header.createCell(5); - h6.setCellValue("Organisation Name"); - h6.setCellStyle(readOnlyHeader); - - var h7 = header.createCell(6); - h7.setCellValue("Facility"); - h7.setCellStyle(boldStyle); - - var h8 = header.createCell(7); - h8.setCellValue("Instrument"); - h8.setCellStyle(boldStyle); - - var h9 = header.createCell(8); - h9.setCellValue("Instrument Name"); - h9.setCellStyle(readOnlyHeader); - - header.createCell(9).setCellValue("Cycle/Fraction Name"); - header.createCell(10).setCellValue("Digestion Method"); - header.createCell(11).setCellValue("Digestion Enzyme"); - header.createCell(12).setCellValue("Enrichment Method"); - header.createCell(13).setCellValue("Injection Volume (uL)"); - header.createCell(14).setCellValue("LC Column"); - header.createCell(15).setCellValue("LCMS Method"); - header.createCell(16).setCellValue("Labeling Type"); - header.createCell(17).setCellValue("Label"); - header.createCell(18).setCellValue("Comment"); - - for (int i = 9; i < 19; i++) { - header.getCell(i).setCellStyle(boldStyle); - } - } - - private static void createMeasurementEntry(ProteomicsMeasurementEntry pxpEntry, Row entry, + private static void createMeasurementEntry(ProteomicsMeasurementEntry pxpEntry, Row entryRow, CellStyle readOnlyStyle) { - var measureCol = entry.createCell(0); - measureCol.setCellValue(pxpEntry.measurementCode()); - measureCol.setCellStyle(readOnlyStyle); - - var sampleIdCol = entry.createCell(1); - sampleIdCol.setCellValue(pxpEntry.sampleInformation().sampleId()); - sampleIdCol.setCellStyle(readOnlyStyle); - - var sampleNameCol = entry.createCell(2); - sampleNameCol.setCellValue(pxpEntry.sampleInformation().sampleName()); - sampleNameCol.setCellStyle(readOnlyStyle); - - var samplePoolCol = entry.createCell(3); - samplePoolCol.setCellValue(pxpEntry.samplePoolGroup()); - samplePoolCol.setCellStyle(readOnlyStyle); - - entry.createCell(4).setCellValue(pxpEntry.organisationId()); - - var organisationNameCol = entry.createCell(5); - organisationNameCol.setCellValue(pxpEntry.organisationName()); - organisationNameCol.setCellStyle(readOnlyStyle); - - entry.createCell(6).setCellValue(pxpEntry.facility()); - entry.createCell(7).setCellValue(pxpEntry.instrumentCURI()); - - var instumentNameCol = entry.createCell(8); - instumentNameCol.setCellValue(pxpEntry.instrumentName()); - instumentNameCol.setCellStyle(readOnlyStyle); - - entry.createCell(9).setCellValue(pxpEntry.fractionName()); - entry.createCell(10).setCellValue(pxpEntry.digestionMethod()); - entry.createCell(11).setCellValue(pxpEntry.digestionEnzyme()); - entry.createCell(12).setCellValue(pxpEntry.enrichmentMethod()); - entry.createCell(13).setCellValue(Integer.parseInt(pxpEntry.injectionVolume())); - entry.createCell(14).setCellValue(pxpEntry.lcColumn()); - entry.createCell(15).setCellValue(pxpEntry.lcmsMethod()); - entry.createCell(16).setCellValue(pxpEntry.labelingType()); - entry.createCell(17).setCellValue(pxpEntry.label()); - entry.createCell(18).setCellValue(pxpEntry.comment()); + for (ProteomicsMeasurementColumns measurementColumn : ProteomicsMeasurementColumns.values()) { + var value = switch (measurementColumn) { + case MEASUREMENT_ID -> pxpEntry.measurementCode(); + case SAMPLE_ID -> pxpEntry.sampleInformation().sampleId(); + case SAMPLE_NAME -> pxpEntry.sampleInformation().sampleName(); + case POOL_GROUP -> pxpEntry.samplePoolGroup(); + case TECHNICAL_REPLICATE_NAME -> pxpEntry.technicalReplicateName(); + case ORGANISATION_ID -> pxpEntry.organisationId(); + case ORGANISATION_NAME -> pxpEntry.organisationName(); + case FACILITY -> pxpEntry.facility(); + case MS_DEVICE -> pxpEntry.msDeviceCURIE(); + case MS_DEVICE_NAME -> pxpEntry.msDeviceName(); + case CYCLE_FRACTION_NAME -> pxpEntry.fractionName(); + case DIGESTION_METHOD -> pxpEntry.digestionMethod(); + case DIGESTION_ENZYME -> pxpEntry.digestionEnzyme(); + case ENRICHMENT_METHOD -> pxpEntry.enrichmentMethod(); + case INJECTION_VOLUME -> pxpEntry.injectionVolume(); + case LC_COLUMN -> pxpEntry.lcColumn(); + case LCMS_METHOD -> pxpEntry.lcmsMethod(); + case LABELING_TYPE -> pxpEntry.labelingType(); + case LABEL -> pxpEntry.label(); + case COMMENT -> pxpEntry.comment(); + }; + var cell = getOrCreateCell(entryRow, measurementColumn.columnIndex()); + cell.setCellValue(value); + if (measurementColumn.readOnly()) { + cell.setCellStyle(readOnlyStyle); + } + } } public void setMeasurements(List measurements, String fileNamePrefix) { @@ -156,26 +105,21 @@ public byte[] getContent() { ByteArrayOutputStream byteArrayOutputStream; try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("Proteomics Measurement Metadata"); - - Row header = sheet.createRow(0); - CellStyle readOnlyHeader = workbook.createCellStyle(); - readOnlyHeader.setFillForegroundColor( + CellStyle readOnlyHeaderStyle = workbook.createCellStyle(); + readOnlyHeaderStyle.setFillForegroundColor( new XSSFColor(LIGHT_GREY, new DefaultIndexedColorMap())); - readOnlyHeader.setFillPattern(FillPatternType.SOLID_FOREGROUND); + readOnlyHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); XSSFFont fontHeader = (XSSFFont) workbook.createFont(); fontHeader.setBold(true); fontHeader.setColor(new XSSFColor(DARK_GREY, new DefaultIndexedColorMap())); - readOnlyHeader.setFont(fontHeader); + readOnlyHeaderStyle.setFont(fontHeader); CellStyle boldStyle = workbook.createCellStyle(); Font fontBold = workbook.createFont(); fontBold.setBold(true); boldStyle.setFont(fontBold); - formatHeader(header, readOnlyHeader, boldStyle); - CellStyle readOnlyStyle = workbook.createCellStyle(); readOnlyStyle.setFillForegroundColor(new XSSFColor(LIGHT_GREY, new DefaultIndexedColorMap())); readOnlyStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); @@ -183,15 +127,45 @@ public byte[] getContent() { font.setColor(new XSSFColor(DARK_GREY, new DefaultIndexedColorMap())); readOnlyStyle.setFont(font); - int rowCounter = 1; + Sheet sheet = workbook.createSheet("Proteomics Measurement Metadata"); + Row header = getOrCreateRow(sheet, 0); + for (ProteomicsMeasurementColumns measurementColumn : ProteomicsMeasurementColumns.values()) { + var cell = getOrCreateCell(header, measurementColumn.columnIndex()); + cell.setCellValue(measurementColumn.headerName()); + if (measurementColumn.readOnly()) { + cell.setCellStyle(readOnlyHeaderStyle); + } else { + cell.setCellStyle(boldStyle); + } + } + + var startIndex = 1; // start in row number 2 with index 1 skipping the header in the first row + var rowIndex = startIndex; for (ProteomicsMeasurementEntry pxpEntry : measurements) { - Row entry = sheet.createRow(rowCounter); + Row entry = getOrCreateRow(sheet, rowIndex); createMeasurementEntry(pxpEntry, entry, readOnlyStyle); - rowCounter++; + rowIndex++; } + var generatedRowCount = rowIndex - startIndex; + assert generatedRowCount == measurements.size() : "all measurements have a corresponding row"; + + // make sure to create the visible sheet first + Sheet hiddenSheet = workbook.createSheet("hidden"); + Name digestionMethodArea = createOptionArea(hiddenSheet, "Digestion Method", + DigestionMethod.getOptions()); + + addDataValidation(sheet, + ProteomicsMeasurementColumns.DIGESTION_METHOD.columnIndex(), startIndex, + ProteomicsMeasurementColumns.DIGESTION_METHOD.columnIndex(), + DEFAULT_GENERATED_ROW_COUNT - 1, + digestionMethodArea); setAutoWidth(sheet); + workbook.setActiveSheet(0); + + lockSheet(hiddenSheet); + hideSheet(workbook, hiddenSheet); byteArrayOutputStream = new ByteArrayOutputStream(); workbook.write(byteArrayOutputStream); @@ -203,6 +177,53 @@ public byte[] getContent() { return byteArrayOutputStream.toByteArray(); } + enum ProteomicsMeasurementColumns { + + MEASUREMENT_ID("Measurement ID", 0, true), + SAMPLE_ID("QBiC Sample Id", 1, true), + SAMPLE_NAME( + "Sample Name", 2, true), + POOL_GROUP("Sample Pool Group", 3, true), + TECHNICAL_REPLICATE_NAME("Technical Replicate", 4, false), + ORGANISATION_ID("Organisation ID", 5, false), + ORGANISATION_NAME("Organisation Name", 6, true), + FACILITY("Facility", 7, false), + MS_DEVICE("MS Device", 8, false), + MS_DEVICE_NAME("MS Device Name", 9, true), + CYCLE_FRACTION_NAME("Cycle/Fraction Name", 10, false), + DIGESTION_METHOD("Digestion Method", 11, false), + DIGESTION_ENZYME("Digestion Enzyme", 12, false), + ENRICHMENT_METHOD("Enrichment Method", 13, false), + INJECTION_VOLUME("Injection Volume (µL)", 14, false), + LC_COLUMN("LC Column", 15, false), + LCMS_METHOD("LCMS Method", 16, false), + LABELING_TYPE("Labeling Type", 17, false), + LABEL("Label", 18, false), + COMMENT("Comment", 19, false), + ; + private final String headerName; + private final int columnIndex; + private final boolean readOnly; + + ProteomicsMeasurementColumns(String headerName, int columnIndex, boolean readOnly) { + this.headerName = headerName; + this.columnIndex = columnIndex; + this.readOnly = readOnly; + } + + public String headerName() { + return headerName; + } + + public int columnIndex() { + return columnIndex; + } + + public boolean readOnly() { + return readOnly; + } + } + @Override public String getFileName() { return String.join("_", fileNamePrefix, FILE_NAME_SUFFIX); diff --git a/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx b/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx index fa8406e3bb..644b9ae866 100644 Binary files a/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx and b/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx differ diff --git a/user-interface/src/test/java/life/qbic/datamanager/spreadsheet/XLSXTemplateHelperTest.groovy b/user-interface/src/test/java/life/qbic/datamanager/spreadsheet/XLSXTemplateHelperTest.groovy new file mode 100644 index 0000000000..2e7ef29203 --- /dev/null +++ b/user-interface/src/test/java/life/qbic/datamanager/spreadsheet/XLSXTemplateHelperTest.groovy @@ -0,0 +1,39 @@ +package life.qbic.datamanager.spreadsheet + + +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.ss.usermodel.WorkbookFactory +import spock.lang.Specification + +class XLSXTemplateHelperTest extends Specification { + def "test to camel case"() { + expect: + XLSXTemplateHelper.toCamelCase(input).equals(output) + + where: + input | output + "this is a test" | "thisIsATest" + "this is 4 test" | "thisIs4Test" + "this-is-a-test" | "thisIsATest" + "this_is_a_test" | "thisIsATest" + "this is_a-test" | "thisIsATest" + "thisisatest" | "thisisatest" + "thisIsATest" | "thisIsATest" + "this is a test*" | "thisIsATest" + + } + + def "test that column reference works"() { + given: + Workbook workbook = WorkbookFactory.create(true) + def sheet = workbook.createSheet("My sheet") + when: + var result = XLSXTemplateHelper.createOptionArea(sheet, + "test values", + List.of("test1", "test2", "aböüß")) + then: + result.getRefersToFormula() == "'My sheet'!\$A\$1:\$A\$4" + result.getNameName() == "testValues" + workbook.getName("testValues") != null + } +}