diff --git a/repository-services/isajson-biosamples/src/main/java/com/elixir/biohackaton/ISAToSRA/biosamples/service/BioSamplesSubmitter.java b/repository-services/isajson-biosamples/src/main/java/com/elixir/biohackaton/ISAToSRA/biosamples/service/BioSamplesSubmitter.java index f3ce3648..84a560f2 100644 --- a/repository-services/isajson-biosamples/src/main/java/com/elixir/biohackaton/ISAToSRA/biosamples/service/BioSamplesSubmitter.java +++ b/repository-services/isajson-biosamples/src/main/java/com/elixir/biohackaton/ISAToSRA/biosamples/service/BioSamplesSubmitter.java @@ -9,7 +9,6 @@ import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; import java.time.Instant; import java.util.*; -import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.hateoas.EntityModel; @@ -23,42 +22,52 @@ @Service @Slf4j public class BioSamplesSubmitter { + private static final String ORGANISM_ATTRIBUTE = "organism"; + private static final String TAX_ID_ATTRIBUTE = "tax_id"; + private static final String COLLECTION_DATE_ATTRIBUTE = "collection date"; + private static final String GEOGRAPHIC_LOCATION_ATTRIBUTE = + "geographic location (country and/or sea)"; + public BiosampleAccessionsMap createBioSamples( final List studies, final String webinToken) { final BiosampleAccessionsMap typeToBioSamplesAccessionMap = new BiosampleAccessionsMap(); try { - /*TODO: check if it is guaranteed to have one source */ - final BioSample sourceBioSample = this.createSourceBioSample(studies, webinToken).get(0); - typeToBioSamplesAccessionMap.sourceAccessionsMap.isaItemName = Source.Fields.name; - typeToBioSamplesAccessionMap.sourceAccessionsMap.accessionMap.put( - sourceBioSample.getName(), sourceBioSample.getAccession()); - - studies.forEach( - study -> { - final Map characteristicKeyLookup = - buildCharacteristicKeyLookup(study); - typeToBioSamplesAccessionMap.studyAccessionsMap = - new ReceiptAccessionsMap(Study.Fields.title, study.getTitle()); - - study - .getMaterials() - .getSamples() - .forEach( - sample -> { - final BioSample persistedChildSample = - this.createAndUpdateChildSampleWithRelationship( - sample, sourceBioSample, webinToken, characteristicKeyLookup); - - if (persistedChildSample != null) { - typeToBioSamplesAccessionMap.sampleAccessionsMap.isaItemName = - Sample.Fields.name; - typeToBioSamplesAccessionMap.sampleAccessionsMap.accessionMap.put( - persistedChildSample.getName(), persistedChildSample.getAccession()); - } - }); - }); + typeToBioSamplesAccessionMap.sampleAccessionsMap.isaItemName = Sample.Fields.name; + + for (final Study study : studies) { + final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study); + final Map> protocolToParameterNameMap = + buildProtocolToParameterNameLookup(study); + // Keep the source lookup scoped to the study; ISA material ids are not safely global. + final Map sourceBioSamplesById = + createSourceBioSamplesById(study, characteristicKeyLookup, webinToken); + + addSourceAccessions(typeToBioSamplesAccessionMap, sourceBioSamplesById.values()); + typeToBioSamplesAccessionMap.studyAccessionsMap = + new ReceiptAccessionsMap(Study.Fields.title, study.getTitle()); + + for (final Sample sample : study.getMaterials().getSamples()) { + final ProcessSequence sampleCollectionProcess = + findProcessByOutputId(study.getProcessSequence(), sample.getId()); + final BioSample sourceBioSample = + findSourceBioSampleForSample(sample, sampleCollectionProcess, sourceBioSamplesById); + final BioSample persistedChildSample = + this.createAndUpdateChildSampleWithRelationship( + sample, + sourceBioSample, + webinToken, + characteristicKeyLookup, + protocolToParameterNameMap, + sampleCollectionProcess); + + if (persistedChildSample != null) { + typeToBioSamplesAccessionMap.sampleAccessionsMap.accessionMap.put( + persistedChildSample.getName(), persistedChildSample.getAccession()); + } + } + } } catch (final Exception e) { throw new RuntimeException("Failed to parse ISA Json and create samples in BioSamples", e); } @@ -70,14 +79,16 @@ private BioSample createAndUpdateChildSampleWithRelationship( final Sample sample, final BioSample sourceBioSample, final String webinToken, - final Map characteristicKeyLookup) { + final Map characteristicKeyLookup, + final Map> protocolToParameterNameMap, + final ProcessSequence sampleCollectionProcess) { final SortedSet childSampleAttributes = - buildAttributesFromCharacteristics(sample.getCharacteristics(), characteristicKeyLookup); - synchronizeSharedAttribute(childSampleAttributes, sourceBioSample, "organism"); - synchronizeSharedAttribute(childSampleAttributes, sourceBioSample, "tax_id"); - copySourceAttributeIfMissing(childSampleAttributes, sourceBioSample, "collection date"); - copySourceAttributeIfMissing( - childSampleAttributes, sourceBioSample, "geographic location (country and/or sea)"); + buildChildSampleAttributes( + sample, + sourceBioSample, + characteristicKeyLookup, + protocolToParameterNameMap, + sampleCollectionProcess); final BioSample bioSample = new BioSample.Builder(sample.getName() != null ? sample.getName() : "child_sample") .withRelease(Instant.now()) @@ -106,44 +117,128 @@ private BioSample createAndUpdateChildSampleWithRelationship( } } - private List createSourceBioSample( - final List studies, final String webinToken) { - List biosamples = new ArrayList<>(); - - studies.forEach( - study -> - study - .getMaterials() - .getSources() - .forEach( - source -> { - final Map characteristicKeyLookup = - buildCharacteristicKeyLookup(study); - final List attributes = new ArrayList<>(); - source - .getCharacteristics() - .forEach( - characteristic -> { - if (characteristic.getCategory().getId() != null) { - final String extractedKey = - getCharacteristicKey( - characteristic.getCategory(), characteristicKeyLookup); - - attributes.add( - Attribute.build( - extractedKey, - characteristic.getValue().getAnnotationValue())); - } - }); - final BioSample sourceSample = - new BioSample.Builder(source.getName()) - .withRelease(Instant.now()) - .withAttributes(attributes) - .build(); - biosamples.add(this.createSampleInBioSamples(sourceSample, webinToken)); - })); - - return biosamples; + private SortedSet buildChildSampleAttributes( + final Sample sample, + final BioSample sourceBioSample, + final Map characteristicKeyLookup, + final Map> protocolToParameterNameMap, + final ProcessSequence sampleCollectionProcess) { + final SortedSet childSampleAttributes = + buildAttributesFromCharacteristics(sample.getCharacteristics(), characteristicKeyLookup); + + // Direct sample characteristics are the most specific metadata; process parameters only fill + // BioSample attributes that are not already present on the output sample. + addAttributesIfMissing( + childSampleAttributes, + buildAttributesFromProcessParameters( + sampleCollectionProcess, protocolToParameterNameMap)); + + synchronizeSharedAttribute(childSampleAttributes, sourceBioSample, ORGANISM_ATTRIBUTE); + synchronizeSharedAttribute(childSampleAttributes, sourceBioSample, TAX_ID_ATTRIBUTE); + copySourceAttributeIfMissing( + childSampleAttributes, sourceBioSample, COLLECTION_DATE_ATTRIBUTE); + copySourceAttributeIfMissing( + childSampleAttributes, sourceBioSample, GEOGRAPHIC_LOCATION_ATTRIBUTE); + + return childSampleAttributes; + } + + private Map createSourceBioSamplesById( + final Study study, + final Map characteristicKeyLookup, + final String webinToken) { + final Map biosamplesBySourceId = new LinkedHashMap<>(); + + for (final Source source : study.getMaterials().getSources()) { + final SortedSet attributes = + buildAttributesFromCharacteristics(source.getCharacteristics(), characteristicKeyLookup); + final BioSample sourceSample = + new BioSample.Builder(source.getName()) + .withRelease(Instant.now()) + .withAttributes(attributes) + .build(); + biosamplesBySourceId.put( + source.getId(), this.createSampleInBioSamples(sourceSample, webinToken)); + } + + return biosamplesBySourceId; + } + + private void addSourceAccessions( + final BiosampleAccessionsMap typeToBioSamplesAccessionMap, + final Collection sourceBioSamples) { + sourceBioSamples.forEach( + sourceBioSample -> + typeToBioSamplesAccessionMap.sourceAccessionsMap.accessionMap.put( + sourceBioSample.getName(), sourceBioSample.getAccession())); + } + + private BioSample findSourceBioSampleForSample( + final Sample sample, + final ProcessSequence sampleCollectionProcess, + final Map sourceBioSamplesById) { + // Prefer explicit material lineage, but fall back to the process input for ISA exports that + // only carry the source-to-sample relationship in processSequence. + final Optional sourceId = + findSourceIdFromSampleDerivesFrom(sample, sourceBioSamplesById) + .or(() -> findSourceIdFromProcessInputs(sampleCollectionProcess, sourceBioSamplesById)); + if (sourceId.isPresent()) { + return sourceBioSamplesById.get(sourceId.get()); + } + + if (sourceBioSamplesById.size() == 1) { + return sourceBioSamplesById.values().iterator().next(); + } + + throw new IllegalArgumentException( + "Could not resolve source BioSample for sample " + sample.getId() + "."); + } + + private Optional findSourceIdFromSampleDerivesFrom( + final Sample sample, final Map sourceBioSamplesById) { + if (sample == null || sample.getDerivesFrom() == null) { + return Optional.empty(); + } + + return sample.getDerivesFrom().stream() + .filter(Objects::nonNull) + .map(DerivesFrom::getId) + .filter(sourceBioSamplesById::containsKey) + .findFirst(); + } + + private Optional findSourceIdFromProcessInputs( + final ProcessSequence processSequence, final Map sourceBioSamplesById) { + if (processSequence == null || processSequence.getInputs() == null) { + return Optional.empty(); + } + + return processSequence.getInputs().stream() + .filter(Objects::nonNull) + .map(Input::getId) + .filter(sourceBioSamplesById::containsKey) + .findFirst(); + } + + private ProcessSequence findProcessByOutputId( + final List processSequence, final String outputId) { + if (processSequence == null || outputId == null) { + return null; + } + + for (final ProcessSequence process : processSequence) { + if (process == null || process.getOutputs() == null) { + continue; + } + + for (final Output output : process.getOutputs()) { + if (output != null && outputId.equals(output.getId())) { + return process; + } + } + } + + return null; } private SortedSet buildAttributesFromCharacteristics( @@ -174,6 +269,47 @@ private SortedSet buildAttributesFromCharacteristics( return attributes; } + private SortedSet buildAttributesFromProcessParameters( + final ProcessSequence processSequence, + final Map> protocolToParameterNameMap) { + final SortedSet attributes = new TreeSet<>(); + + if (processSequence == null + || processSequence.getExecutesProtocol() == null + || processSequence.getExecutesProtocol().getId() == null + || processSequence.getParameterValues() == null) { + return attributes; + } + + final Map parameterNameLookup = + protocolToParameterNameMap.get(processSequence.getExecutesProtocol().getId()); + + if (parameterNameLookup == null) { + return attributes; + } + + processSequence + .getParameterValues() + .forEach( + parameterValue -> { + if (parameterValue == null + || parameterValue.getCategory() == null + || parameterValue.getCategory().getId() == null + || parameterValue.getValue() == null) { + return; + } + + final String key = parameterNameLookup.get(parameterValue.getCategory().getId()); + final String value = parameterValue.getValue().getAnnotationValue(); + + if (key != null && !key.isBlank() && value != null && !value.isBlank()) { + attributes.add(Attribute.build(key, value)); + } + }); + + return attributes; + } + private Map buildCharacteristicKeyLookup(final Study study) { final Map keyLookup = new HashMap<>(); @@ -199,22 +335,86 @@ private Map buildCharacteristicKeyLookup(final Study study) { return keyLookup; } + private Map> buildProtocolToParameterNameLookup(final Study study) { + final Map> protocolToParameterNameMap = new HashMap<>(); + + if (study == null || study.getProtocols() == null) { + return protocolToParameterNameMap; + } + + study + .getProtocols() + .forEach( + protocol -> { + if (protocol == null || protocol.getId() == null) { + return; + } + + final Map parameterNameMap = new HashMap<>(); + + if (protocol.getParameters() != null) { + protocol + .getParameters() + .forEach(parameter -> addParameterName(parameterNameMap, parameter)); + } + + protocolToParameterNameMap.put(protocol.getId(), parameterNameMap); + }); + + return protocolToParameterNameMap; + } + + private void addParameterName( + final Map parameterNameMap, final Parameter parameter) { + if (parameter == null + || parameter.getId() == null + || parameter.getParameterName() == null + || parameter.getParameterName().getAnnotationValue() == null + || parameter.getParameterName().getAnnotationValue().isBlank()) { + return; + } + + parameterNameMap.put(parameter.getId(), parameter.getParameterName().getAnnotationValue()); + } + + private void addAttributesIfMissing( + final SortedSet targetAttributes, final Collection sourceAttributes) { + if (sourceAttributes == null) { + return; + } + + sourceAttributes.forEach(attribute -> addAttributeIfMissing(targetAttributes, attribute)); + } + + private void addAttributeIfMissing( + final SortedSet attributes, final Attribute attribute) { + if (attribute == null || attribute.getType() == null) { + return; + } + + final boolean alreadyPresent = + attributes.stream() + .anyMatch( + existingAttribute -> + existingAttribute.getType().equalsIgnoreCase(attribute.getType())); + + if (!alreadyPresent) { + attributes.add(attribute); + } + } + private void copySourceAttributeIfMissing( final SortedSet childAttributes, final BioSample sourceBioSample, final String attributeType) { - final boolean alreadyPresent = - childAttributes.stream() - .anyMatch(attribute -> attribute.getType().equalsIgnoreCase(attributeType)); - - if (alreadyPresent || sourceBioSample.getAttributes() == null) { + if (sourceBioSample.getAttributes() == null) { return; } sourceBioSample.getAttributes().stream() .filter(attribute -> attribute.getType().equalsIgnoreCase(attributeType)) .findFirst() - .ifPresent(childAttributes::add); + .ifPresent(attribute -> addAttributeIfMissing(childAttributes, attribute)); } private void synchronizeSharedAttribute( @@ -243,35 +443,47 @@ private void synchronizeSharedAttribute( } } - private static Characteristic getBioSampleAccessionCharacteristic( - AtomicReference biosample) { - final Characteristic biosampleAccessionCharacteristic = new Characteristic(); - final Category biosampleAccessionCategory = new Category(); - final Value biosampleAccessionValue = new Value(); - - biosampleAccessionCategory.setId("#characteristic_category/accession"); - biosampleAccessionValue.setAnnotationValue(biosample.get().getAccession()); - - biosampleAccessionCharacteristic.setCategory(biosampleAccessionCategory); - biosampleAccessionCharacteristic.setValue(biosampleAccessionValue); - - return biosampleAccessionCharacteristic; - } - /** * Uses the study-level ISA characteristic definition to resolve the human-readable attribute * name for a characteristic id. */ private static String getCharacteristicKey( final Category category, final Map characteristicKeyLookup) { - if (category == null || category.getId() == null) { + if (category == null) { return null; } - return characteristicKeyLookup.get(category.getId()); + if (category.getId() != null) { + final String characteristicName = characteristicKeyLookup.get(category.getId()); + if (characteristicName != null && !characteristicName.isBlank()) { + return characteristicName; + } + } + + if (category.getCharacteristicType() != null + && category.getCharacteristicType().getAnnotationValue() != null + && !category.getCharacteristicType().getAnnotationValue().isBlank()) { + return category.getCharacteristicType().getAnnotationValue(); + } + + return characteristicCategoryIdToName(category.getId()); + } + + private static String characteristicCategoryIdToName(final String characteristicCategoryId) { + if (characteristicCategoryId == null || characteristicCategoryId.isBlank()) { + return null; + } + + final String prefix = "#characteristic_category/"; + final String characteristicName = + characteristicCategoryId.startsWith(prefix) + ? characteristicCategoryId.substring(prefix.length()) + : characteristicCategoryId; + + return characteristicName.replaceFirst("_[0-9]+$", ""); } - private BioSample updateSampleWithRelationshipsToBioSamples( + protected BioSample updateSampleWithRelationshipsToBioSamples( final BioSample sampleWithRelationship, final String webinToken) { final RestTemplate restTemplate = new RestTemplate(); final ResponseEntity> biosamplesResponse; @@ -293,7 +505,7 @@ private BioSample updateSampleWithRelationshipsToBioSamples( } } - private BioSample createSampleInBioSamples(final BioSample sample, final String webinToken) { + protected BioSample createSampleInBioSamples(final BioSample sample, final String webinToken) { final RestTemplate restTemplate = new RestTemplate(); final ResponseEntity> biosamplesResponse; diff --git a/repository-services/isajson-biosamples/src/test/java/com/elixir/biohackaton/ISAToSRA/BiosampleReceiptToMarsTest.java b/repository-services/isajson-biosamples/src/test/java/com/elixir/biohackaton/ISAToSRA/BiosampleReceiptToMarsTest.java index 42e064ff..33d1d12d 100644 --- a/repository-services/isajson-biosamples/src/test/java/com/elixir/biohackaton/ISAToSRA/BiosampleReceiptToMarsTest.java +++ b/repository-services/isajson-biosamples/src/test/java/com/elixir/biohackaton/ISAToSRA/BiosampleReceiptToMarsTest.java @@ -1,6 +1,7 @@ /** Elixir BioHackathon 2022 */ package com.elixir.biohackaton.ISAToSRA; +import com.elixir.biohackaton.ISAToSRA.biosamples.model.BioSample; import com.elixir.biohackaton.ISAToSRA.biosamples.model.BiosampleAccessionsMap; import com.elixir.biohackaton.ISAToSRA.biosamples.service.BioSamplesSubmitter; import com.elixir.biohackaton.ISAToSRA.biosamples.service.MarsReceiptService; @@ -11,7 +12,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.nio.file.Files; +import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -54,4 +57,157 @@ void convertToMars() { System.console().printf("%s", ex); } } + + @Test + void createBioSamplesUsesSampleCollectionProcessParametersForChildSampleAttributes() + throws Exception { + final String isaJsonFilePath = "../../test-data/biosamples-input-isa.json"; + final String isaJsonFile = Files.readString(new File(isaJsonFilePath).toPath()); + final ObjectMapper objectMapper = new ObjectMapper(); + final IsaJson isaJson = objectMapper.readValue(isaJsonFile, IsaJson.class); + final CapturingBioSamplesSubmitter bioSamplesSubmitter = new CapturingBioSamplesSubmitter(); + + bioSamplesSubmitter.createBioSamples(isaJson.getInvestigation().getStudies(), "test-token"); + + final BioSample leafSample = + bioSamplesSubmitter.updatedSamples.stream() + .filter(sample -> "leaf 1".equals(sample.getName())) + .findFirst() + .orElseThrow(); + final BioSample sourceSample = bioSamplesSubmitter.createdSampleByName("plant 1"); + + Assertions.assertTrue(hasAttribute(sourceSample, "genotype", "Col-0")); + Assertions.assertTrue( + hasAttribute(sourceSample, "growth condition", "16 h light / 8 h dark growth chamber")); + Assertions.assertTrue(hasAttribute(sourceSample, "dev_stage", "vegetative rosette stage")); + Assertions.assertTrue(hasAttribute(sourceSample, "isolation_source", "whole plant")); + Assertions.assertTrue(hasAttribute(leafSample, "organism part", "leaf")); + Assertions.assertTrue( + hasAttribute( + leafSample, + "sample description", + "young rosette leaf collected for DNA extraction")); + Assertions.assertTrue( + leafSample.getAttributes().stream() + .anyMatch( + attribute -> + "isolation_source".equals(attribute.getType()) + && "leaf tissue".equals(attribute.getValue())), + "Expected child BioSample to include the sample-collection process parameter."); + Assertions.assertTrue(hasAttribute(leafSample, "collection method", "sterile scalpel excision")); + Assertions.assertTrue( + hasAttribute(leafSample, "sample preservation", "flash frozen in liquid nitrogen")); + } + + @Test + void createBioSamplesKeepsEachChildSampleLinkedToItsOwnSource() throws Exception { + final String isaJsonFilePath = "../../test-data/biosamples-input-isa-multi.json"; + final String isaJsonFile = Files.readString(new File(isaJsonFilePath).toPath()); + final ObjectMapper objectMapper = new ObjectMapper(); + final IsaJson isaJson = objectMapper.readValue(isaJsonFile, IsaJson.class); + final CapturingBioSamplesSubmitter bioSamplesSubmitter = new CapturingBioSamplesSubmitter(); + + bioSamplesSubmitter.createBioSamples(isaJson.getInvestigation().getStudies(), "test-token"); + + final BioSample leafSample = bioSamplesSubmitter.updatedSampleByName("leaf 1"); + final BioSample rootSample = bioSamplesSubmitter.updatedSampleByName("root 1"); + + Assertions.assertEquals("SAMEA_TEST_1", derivedFromTarget(leafSample)); + Assertions.assertEquals("SAMEA_TEST_2", derivedFromTarget(rootSample)); + Assertions.assertTrue(hasAttribute(rootSample, "collection date", "2022-01-03")); + Assertions.assertTrue(hasAttribute(rootSample, "organism part", "root")); + Assertions.assertTrue(hasAttribute(rootSample, "isolation_source", "root tissue")); + Assertions.assertTrue(hasAttribute(rootSample, "collection method", "washed root excision")); + Assertions.assertTrue( + hasAttribute(rootSample, "sample preservation", "flash frozen in liquid nitrogen")); + } + + @Test + void createBioSamplesSupportsMultipleChildSamplesFromTheSameSource() throws Exception { + final String isaJsonFilePath = "../../test-data/biosamples-input-isa-multi.json"; + final String isaJsonFile = Files.readString(new File(isaJsonFilePath).toPath()); + final ObjectMapper objectMapper = new ObjectMapper(); + final IsaJson isaJson = objectMapper.readValue(isaJsonFile, IsaJson.class); + final Study study = isaJson.getInvestigation().getStudies().get(0); + + study.getMaterials().getSamples().stream() + .filter(sample -> "root 1".equals(sample.getName())) + .findFirst() + .orElseThrow() + .getDerivesFrom() + .get(0) + .setId("#source/330"); + study.getProcessSequence().stream() + .filter(process -> "#process/sample_collection/431".equals(process.getId())) + .findFirst() + .orElseThrow() + .getInputs() + .get(0) + .setId("#source/330"); + + final CapturingBioSamplesSubmitter bioSamplesSubmitter = new CapturingBioSamplesSubmitter(); + bioSamplesSubmitter.createBioSamples(isaJson.getInvestigation().getStudies(), "test-token"); + + final BioSample leafSample = bioSamplesSubmitter.updatedSampleByName("leaf 1"); + final BioSample rootSample = bioSamplesSubmitter.updatedSampleByName("root 1"); + + Assertions.assertEquals("SAMEA_TEST_1", derivedFromTarget(leafSample)); + Assertions.assertEquals("SAMEA_TEST_1", derivedFromTarget(rootSample)); + Assertions.assertTrue(hasAttribute(rootSample, "organism part", "root")); + Assertions.assertTrue(hasAttribute(rootSample, "collection method", "washed root excision")); + } + + private static String derivedFromTarget(final BioSample sample) { + return sample.getRelationships().stream() + .filter(relationship -> "derived from".equals(relationship.getType())) + .findFirst() + .orElseThrow() + .getTarget(); + } + + private static boolean hasAttribute( + final BioSample sample, final String attributeType, final String attributeValue) { + return sample.getAttributes().stream() + .anyMatch( + attribute -> + attributeType.equals(attribute.getType()) + && attributeValue.equals(attribute.getValue())); + } + + private static class CapturingBioSamplesSubmitter extends BioSamplesSubmitter { + private final List createdSamples = new ArrayList<>(); + private final List updatedSamples = new ArrayList<>(); + private int accessionSequence = 1; + + @Override + protected BioSample createSampleInBioSamples(final BioSample sample, final String webinToken) { + final BioSample createdSample = + BioSample.Builder.fromSample(sample) + .withAccession("SAMEA_TEST_" + accessionSequence++) + .build(); + createdSamples.add(createdSample); + return createdSample; + } + + @Override + protected BioSample updateSampleWithRelationshipsToBioSamples( + final BioSample sampleWithRelationship, final String webinToken) { + updatedSamples.add(sampleWithRelationship); + return sampleWithRelationship; + } + + private BioSample updatedSampleByName(final String sampleName) { + return updatedSamples.stream() + .filter(sample -> sampleName.equals(sample.getName())) + .findFirst() + .orElseThrow(); + } + + private BioSample createdSampleByName(final String sampleName) { + return createdSamples.stream() + .filter(sample -> sampleName.equals(sample.getName())) + .findFirst() + .orElseThrow(); + } + } } diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/controller/WebinIsaToXmlSubmissionController.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/controller/WebinIsaToXmlSubmissionController.java index 7e95e4d0..f691dd17 100644 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/controller/WebinIsaToXmlSubmissionController.java +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/controller/WebinIsaToXmlSubmissionController.java @@ -11,7 +11,6 @@ import com.elixir.biohackaton.ISAToSRA.sra.service.ReceiptConversionService; import com.elixir.biohackaton.ISAToSRA.sra.service.WebinExperimentXmlCreator; import com.elixir.biohackaton.ISAToSRA.sra.service.WebinHttpSubmissionService; -import com.elixir.biohackaton.ISAToSRA.sra.service.WebinProjectXmlCreator; import com.elixir.biohackaton.ISAToSRA.sra.service.WebinRunXmlCreator; import com.elixir.biohackaton.ISAToSRA.sra.service.WebinStudyXmlCreator; import com.fasterxml.jackson.core.type.TypeReference; @@ -41,14 +40,14 @@ * *
    *
  1. Parse ISA-JSON payload - *
  2. Convert ISA-JSON elements to ENA XML (Study, Project, Experiment, Run) + *
  3. Convert ISA-JSON elements to ENA XML (Study, Experiment, Run) *
  4. Submit XML to ENA Webin API *
  5. Convert ENA receipt to MARS receipt format *
  6. Return MARS receipt as JSON *
* *

The conversion follows a bottom-up approach for Experiments and Runs (starting from DataFiles - * and working up), while Study and Project use a top-down approach. + * and working up), while Study uses a top-down approach. */ @Slf4j @RestController @@ -57,8 +56,6 @@ public class WebinIsaToXmlSubmissionController { @Autowired private WebinExperimentXmlCreator webinExperimentXmlCreator; - @Autowired private WebinProjectXmlCreator webinProjectXmlCreator; - @Autowired private WebinRunXmlCreator webinRunXmlCreator; @Autowired private WebinHttpSubmissionService webinHttpSubmissionService; @@ -90,7 +87,6 @@ public class WebinIsaToXmlSubmissionController { *

  • Get BioSamples accessions for study samples *
  • Convert Library → ENA EXPERIMENT (bottom-up: DataFile → Library) *
  • Convert DataFile → ENA RUN (bottom-up: DataFile → Experiment reference) - *
  • Convert Investigation → ENA PROJECT (top-down) *
  • Submit XML to ENA Webin API *
  • Convert ENA receipt to MARS receipt format * @@ -154,27 +150,23 @@ public String performSubmissionToEna( this.webinRunXmlCreator.createENARunSetElement( webinElement, studies, experimentSequenceMap, randomSubmissionIdentifier); - // Step 5: Convert Investigation → ENA PROJECT (top-down approach) - this.webinProjectXmlCreator.createENAProjectSetElement( - webinElement, getInvestigation(isaJson), randomSubmissionIdentifier); - // Debug: Print generated XML to console final OutputFormat format = OutputFormat.createPrettyPrint(); final XMLWriter writer = new XMLWriter(System.out, format); writer.write(document); - // Step 6: Submit XML to ENA Webin API + // Step 5: Submit XML to ENA Webin API final String receiptXml = webinHttpSubmissionService.performWebinSubmission( webinUserName, document.asXML(), webinPassword); - // Step 7: Convert ENA XML receipt to JSON + // Step 6: Convert ENA XML receipt to JSON final Receipt receiptJson = receiptConversionService.readReceiptXml(receiptXml); - // Step 8: Convert ENA receipt to MARS receipt format + // Step 7: Convert ENA receipt to MARS receipt format final MarsReceipt marsReceipt = marsReceiptService.convertReceiptToMars(receiptJson, isaJson); - // Step 9: Return MARS receipt as JSON + // Step 8: Return MARS receipt as JSON return marsReceiptService.convertMarsReceiptToJson(marsReceipt); } @@ -194,27 +186,11 @@ public List getStudies(final IsaJson isaJson) { return null; } - /** - * Extracts Investigation object from ISA-JSON. - * - * @param isaJson the parsed ISA-JSON object - * @return Investigation object, or null if extraction fails - */ - public Investigation getInvestigation(final IsaJson isaJson) { - try { - return isaJson.getInvestigation(); - } catch (final Exception e) { - log.error("Failed to parse ISA JSON and get investigation", e); - } - - return null; - } - /** * Parses BioSamples accessions from input parameter or extracts from ISA-JSON. * *

    If the bioSampleAccessions JSON string is provided, it will be parsed and used. Otherwise, - * falls back to extracting from ISA-JSON Study samples first, then sources. + * falls back to extracting from ISA-JSON Study samples. * *

    Expected format: JSON string with "SOURCE" as key, e.g., {@code {"SOURCE":"SAMEA130793922"}} * diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/IsaJsonGraphLookup.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/IsaJsonGraphLookup.java new file mode 100644 index 00000000..73a3ab6b --- /dev/null +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/IsaJsonGraphLookup.java @@ -0,0 +1,140 @@ +/** Elixir BioHackathon 2022 */ +package com.elixir.biohackaton.ISAToSRA.sra.service; + +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.DataFile; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Input; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Materials; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.OtherMaterial; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Output; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ProcessSequence; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Shared ISA-JSON graph helpers for ENA conversion. + * + *

    Experiment XML, run XML, and receipt conversion all need to resolve the same process/data-file + * lineage. Keeping the lookup rules here avoids subtle drift, especially around ISA's + * #data_file/... versus #data/... ID forms. + */ +final class IsaJsonGraphLookup { + private static final String DATA_FILE_ID_PREFIX = "#data_file/"; + private static final String DATA_ID_PREFIX = "#data/"; + + private IsaJsonGraphLookup() {} + + /** Finds the process that produced an output material or data file. */ + static ProcessSequence findProcessByOutputId( + final List processSequence, final String outputId) { + if (processSequence == null || outputId == null) { + return null; + } + + final String normalizedOutputId = normalizeDataFileId(outputId); + + for (final ProcessSequence process : processSequence) { + if (process == null || process.getOutputs() == null) { + continue; + } + + for (final Output output : process.getOutputs()) { + if (output == null || output.getId() == null) { + continue; + } + + if (normalizeDataFileId(output.getId()).equals(normalizedOutputId)) { + return process; + } + } + } + + return null; + } + + static OtherMaterial findOtherMaterialFromProcessInput( + final ProcessSequence process, final Materials materials) { + if (process == null || process.getInputs() == null) { + return null; + } + + final Map otherMaterialsById = buildOtherMaterialsById(materials); + for (final Input input : process.getInputs()) { + if (input == null || input.getId() == null) { + continue; + } + + final OtherMaterial otherMaterial = otherMaterialsById.get(input.getId()); + if (otherMaterial != null) { + return otherMaterial; + } + } + + return null; + } + + static List findDataFilesFromProcessOutputs( + final ProcessSequence process, final List assayDataFiles) { + final List dataFiles = new ArrayList<>(); + if (process == null || process.getOutputs() == null || assayDataFiles == null) { + return dataFiles; + } + + for (final Output output : process.getOutputs()) { + if (output == null || output.getId() == null) { + continue; + } + + final String normalizedOutputId = normalizeDataFileId(output.getId()); + for (final DataFile dataFile : assayDataFiles) { + if (dataFile == null || dataFile.getId() == null) { + continue; + } + + if (normalizeDataFileId(dataFile.getId()).equals(normalizedOutputId)) { + dataFiles.add(dataFile); + } + } + } + + return dataFiles; + } + + static List normalizedOutputIds(final ProcessSequence process) { + final List outputIds = new ArrayList<>(); + if (process == null || process.getOutputs() == null) { + return outputIds; + } + + for (final Output output : process.getOutputs()) { + if (output == null || output.getId() == null || output.getId().isBlank()) { + continue; + } + + outputIds.add(normalizeDataFileId(output.getId())); + } + + return outputIds; + } + + static Map buildOtherMaterialsById(final Materials materials) { + final Map otherMaterialsById = new HashMap<>(); + if (materials == null || materials.getOtherMaterials() == null) { + return otherMaterialsById; + } + + for (final OtherMaterial otherMaterial : materials.getOtherMaterials()) { + if (otherMaterial != null && otherMaterial.getId() != null) { + otherMaterialsById.put(otherMaterial.getId(), otherMaterial); + } + } + + return otherMaterialsById; + } + + /** Treat ISA #data_file/... and process #data/... references as equivalent. */ + static String normalizeDataFileId(final String id) { + return id == null ? null : id.replace(DATA_FILE_ID_PREFIX, DATA_ID_PREFIX); + } +} diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/MarsReceiptService.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/MarsReceiptService.java index c5609490..6f613af1 100644 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/MarsReceiptService.java +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/MarsReceiptService.java @@ -4,10 +4,11 @@ import com.elixir.biohackaton.ISAToSRA.receipt.MarsReceiptException; import com.elixir.biohackaton.ISAToSRA.receipt.MarsReceiptProvider; import com.elixir.biohackaton.ISAToSRA.receipt.ReceiptAccessionsMap; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.DataFile; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.IsaJson; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.OtherMaterial; -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ProcessSequence; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsError; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsErrorType; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsReceipt; @@ -18,8 +19,11 @@ import com.fasterxml.jackson.databind.SerializationFeature; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -82,12 +86,14 @@ public void setMarsReceiptErrors(MarsError... errors) { public MarsReceipt convertReceiptToMars(final Receipt receipt, final IsaJson isaJson) { buildMarsReceipt( getAliasAccessionPairs( - Study.Fields.title, + // ENA study/project aliases are assay-based, so the returned accession path points to + // the assay rather than the parent study title. + Assay.Fields.id, Optional.ofNullable(receipt.getStudies()).orElse(receipt.getProjects())), null, null, getAliasAccessionPairs(OtherMaterial.Fields.id, receipt.getExperiments()), - getAliasAccessionPairs(DataFile.Fields.id, receipt.getRuns()), + getRunAliasAccessionPairs(receipt.getRuns(), isaJson), receipt.getMessages().getInfoMessages(), receipt.getMessages().getErrorMessages(), isaJson); @@ -112,6 +118,102 @@ private ReceiptAccessionsMap getAliasAccessionPairs( }; } + private ReceiptAccessionsMap getRunAliasAccessionPairs( + final List items, final IsaJson isaJson) { + Predicate aliasAccessionPairValidateFn = this::aliasAccessionPairFilter; + final Map runAccessionMap = new HashMap<>(); + final List> sequencingProcessDataFiles = + getSequencingProcessDataFileIdsInSubmissionOrder(isaJson); + int sequencingProcessIndex = 0; + + for (ReceiptObject receiptObject : Optional.ofNullable(items).orElse(new ArrayList<>())) { + if (!aliasAccessionPairValidateFn.test(receiptObject)) { + continue; + } + + final String processId = getPreRandomizedAlias(receiptObject); + List dataFileIds = getDataFileIdsForSequencingProcess(isaJson, processId); + if (dataFileIds.isEmpty() && sequencingProcessIndex < sequencingProcessDataFiles.size()) { + dataFileIds = sequencingProcessDataFiles.get(sequencingProcessIndex); + } + + dataFileIds.forEach( + dataFileId -> runAccessionMap.put(dataFileId, receiptObject.getAccession())); + sequencingProcessIndex++; + } + + return new ReceiptAccessionsMap() { + { + isaItemName = DataFile.Fields.id; + this.accessionMap = new HashMap<>(runAccessionMap); + } + }; + } + + private List getDataFileIdsForSequencingProcess( + final IsaJson isaJson, final String sequencingProcessId) { + final List dataFileIds = new ArrayList<>(); + + Optional.ofNullable(isaJson.getInvestigation()) + .map(investigation -> investigation.getStudies()) + .orElse(new ArrayList<>()) + .forEach( + study -> + Optional.ofNullable(study.getAssays()) + .orElse(new ArrayList<>()) + .forEach( + assay -> + Optional.ofNullable(assay.getProcessSequence()) + .orElse(new ArrayList<>()) + .stream() + .filter(process -> sequencingProcessId.equals(process.getId())) + .findFirst() + .ifPresent( + process -> + IsaJsonGraphLookup.normalizedOutputIds(process) + .forEach(dataFileIds::add)))); + + return dataFileIds; + } + + private List> getSequencingProcessDataFileIdsInSubmissionOrder(final IsaJson isaJson) { + final List> sequencingProcessDataFiles = new ArrayList<>(); + final Set processedSequencingProcesses = new HashSet<>(); + + Optional.ofNullable(isaJson.getInvestigation()) + .map(investigation -> investigation.getStudies()) + .orElse(new ArrayList<>()) + .forEach( + study -> + Optional.ofNullable(study.getAssays()) + .orElse(new ArrayList<>()) + .forEach(assay -> addAssaySequencingProcessOutputs( + sequencingProcessDataFiles, processedSequencingProcesses, assay))); + + return sequencingProcessDataFiles; + } + + private void addAssaySequencingProcessOutputs( + final List> sequencingProcessDataFiles, + final Set processedSequencingProcesses, + final Assay assay) { + if (assay.getDataFiles() == null || assay.getProcessSequence() == null) { + return; + } + + for (final DataFile dataFile : assay.getDataFiles()) { + final ProcessSequence sequencingProcess = + IsaJsonGraphLookup.findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); + if (sequencingProcess == null || !processedSequencingProcesses.add(sequencingProcess.getId())) { + continue; + } + + final List dataFileIds = new ArrayList<>(); + IsaJsonGraphLookup.normalizedOutputIds(sequencingProcess).forEach(dataFileIds::add); + sequencingProcessDataFiles.add(dataFileIds); + } + } + private boolean aliasAccessionPairFilter(ReceiptObject item) { if (item == null) { setMarsReceiptErrors("ENA receipt: Item is NULL"); @@ -131,7 +233,7 @@ private boolean aliasAccessionPairFilter(ReceiptObject item) { } private String getPreRandomizedAlias(@NotNull ReceiptObject receiptObject) { - // Convert Arabidopsis thaliana-0.49105604184136276 -> Arabidopsis thaliana + // Convert #assay/18_20_21-0.49105604184136276 -> #assay/18_20_21 final String alias = receiptObject.getAlias(); final int lastIndexOfAcceptableAlias = alias.lastIndexOf('-'); return alias.substring( diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/SRAAnalysisXmlCreator.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/SRAAnalysisXmlCreator.java index 2456ab24..b31b34a1 100644 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/SRAAnalysisXmlCreator.java +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/SRAAnalysisXmlCreator.java @@ -4,8 +4,6 @@ import com.elixir.biohackaton.ISAToSRA.receipt.MarsReceiptException; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; import org.dom4j.Element; import org.springframework.stereotype.Service; @@ -34,16 +32,7 @@ private void convertAssayToAnalysisElement(final Assay assay, final Element anal // TODO top level analysis attributes (including type) - // Add samples - assay - .getMaterials() - .getSamples() - .forEach( - sample -> {} // TODO - ); - // add_element(analysis_elemt, 'SAMPLE_REF', - // accession=sample_row.get('Sample Accession'), - // label=sample_row.get('Sample ID')) + // TODO add SAMPLE_REF elements once analysis submissions define sample accession handling. // Add files final Element filesElement = analysisElement.addElement("FILES"); @@ -52,7 +41,9 @@ private void convertAssayToAnalysisElement(final Assay assay, final Element anal private void convertDataFileToFileElement(DataFile dataFile, Element filesElement) { // Analysis must use derived files - if (!dataFile.getType().equalsIgnoreCase(DERIVED_FILE_KEY)) { + if (dataFile == null + || dataFile.getType() == null + || !dataFile.getType().equalsIgnoreCase(DERIVED_FILE_KEY)) { return; } @@ -62,27 +53,35 @@ private void convertDataFileToFileElement(DataFile dataFile, Element filesElemen String filetype = dataFile.getName().substring(dataFile.getName().lastIndexOf('.')); // Files must have a checksum (stored in comments) - AtomicReference checksum = new AtomicReference<>(); - AtomicReference checksumType = new AtomicReference<>(); - dataFile - .getComments() - .forEach( - comment -> { - if (comment.getName().equalsIgnoreCase(CHECKSUM_KEY)) { - checksum.set(comment.getValue()); - } else if (comment.getName().equalsIgnoreCase(CHECKSUM_TYPE_KEY)) { - checksumType.set(comment.getValue()); - } - }); - - if (Objects.isNull(checksum.get()) || Objects.isNull(checksumType.get())) { + final String checksum = findCommentValue(dataFile.getComments(), CHECKSUM_KEY); + final String checksumType = findCommentValue(dataFile.getComments(), CHECKSUM_TYPE_KEY); + + if (checksum == null || checksumType == null) { throw new MarsReceiptException("Checksum and checksum type not found"); - } else { - Element fileElement = filesElement.addElement("FILE"); - fileElement.addAttribute("filename", filename); - fileElement.addAttribute("filetype", filetype); - fileElement.addAttribute("checksum_method", checksumType.get()); - fileElement.addAttribute("checksum", checksum.get()); } + + Element fileElement = filesElement.addElement("FILE"); + fileElement.addAttribute("filename", filename); + fileElement.addAttribute("filetype", filetype); + fileElement.addAttribute("checksum_method", checksumType); + fileElement.addAttribute("checksum", checksum); + } + + private String findCommentValue(final List comments, final String commentName) { + if (comments == null) { + return null; + } + + for (final Comment comment : comments) { + if (comment == null || comment.getName() == null) { + continue; + } + + if (comment.getName().equalsIgnoreCase(commentName)) { + return comment.getValue(); + } + } + + return null; } } diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinExperimentXmlCreator.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinExperimentXmlCreator.java index b0949430..bac6f122 100644 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinExperimentXmlCreator.java +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinExperimentXmlCreator.java @@ -2,10 +2,26 @@ package com.elixir.biohackaton.ISAToSRA.sra.service; import com.elixir.biohackaton.ISAToSRA.receipt.MarsReceiptException; -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Category; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Characteristic; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.CharacteristicCategory; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.DataFile; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.DerivesFrom; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Materials; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.OtherMaterial; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Parameter; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ParameterValue; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ProcessSequence; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Sample; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.dom4j.Element; import org.springframework.stereotype.Service; @@ -13,8 +29,13 @@ @Service @Slf4j public class WebinExperimentXmlCreator { - public static final String OTHER_MATERIAL_LIBRARY_NAME_DETERMINES_EXPERIMENT = "Library Name"; - + /** + * Creates ENA EXPERIMENT XML from ISA-JSON assays. + * + *

    The conversion keeps the original bottom-up approach: start from assay data files, resolve + * the sequencing process that produced them, walk back to the library, and then create one ENA + * experiment per library. + */ public Map createENAExperimentSetElement( final Map typeToBioSamplesAccessionMap, final Element webinElement, @@ -22,13 +43,13 @@ public Map createENAExperimentSetElement( final String randomSubmissionIdentifier) { try { final Element root = webinElement.addElement("EXPERIMENT_SET"); - final Map> protocolToParameterMap = - populateProtocolToParameterMap(studies); + final Map> protocolToParameterNameMap = + populateProtocolToParameterNameMap(studies); return mapExperiments( studies, root, - protocolToParameterMap, + protocolToParameterNameMap, typeToBioSamplesAccessionMap, randomSubmissionIdentifier); } catch (final Exception e) { @@ -37,259 +58,702 @@ public Map createENAExperimentSetElement( } } - private Map> populateProtocolToParameterMap(final List studies) { - final Map> protocolToParameterMap = new HashMap<>(); + private Map> populateProtocolToParameterNameMap( + final List studies) { + final Map> protocolToParameterNameMap = new HashMap<>(); studies.forEach( - study -> - study - .getProtocols() - .forEach( - protocol -> { - protocolToParameterMap.put(protocol.id, protocol.getParameters()); - })); + study -> { + if (study.getProtocols() == null) { + return; + } + + study + .getProtocols() + .forEach( + protocol -> { + final Map parameterNameMap = new HashMap<>(); + + if (protocol.getParameters() != null) { + protocol + .getParameters() + .forEach( + parameter -> addParameterName(parameterNameMap, parameter)); + } + + protocolToParameterNameMap.put(protocol.id, parameterNameMap); + }); + }); - return protocolToParameterMap; + return protocolToParameterNameMap; + } + + private void addParameterName( + final Map parameterNameMap, final Parameter parameter) { + if (parameter == null + || parameter.getId() == null + || parameter.getParameterName() == null + || parameter.getParameterName().getAnnotationValue() == null + || parameter.getParameterName().getAnnotationValue().isBlank()) { + return; + } + + parameterNameMap.put(parameter.getId(), parameter.getParameterName().getAnnotationValue()); } private Map mapExperiments( final List studies, final Element root, - final Map> protocolToParameterMap, + final Map> protocolToParameterNameMap, final Map bioSampleAccessions, final String randomSubmissionIdentifier) { final Map experimentSequence = new HashMap<>(); // Bottom-up approach: Start from DataFiles and work up to Libraries studies.forEach( - study -> - study - .getAssays() - .forEach( - assay -> { - // Start from DataFiles - if (assay.getDataFiles() != null) { - assay - .getDataFiles() - .forEach( - dataFile -> { - // Find the process that produced this data file - final ProcessSequence sequencingProcess = - findProcessByOutputId( - assay.getProcessSequence(), dataFile.getId()); - - if (sequencingProcess != null) { - // Get the Library (OtherMaterial) that was input to sequencing - final OtherMaterial library = - findLibraryFromProcessInput( - sequencingProcess, assay.getMaterials()); - - if (library != null) { - // Create an experiment only once per library - if (!experimentSequence.containsKey(library.getId())) { - final String experimentId = - library.getId() + "-" + randomSubmissionIdentifier; - experimentSequence.put(library.getId(), experimentId); - - // Find the library construction process - final ProcessSequence libraryConstructionProcess = - findProcessByOutputId( - assay.getProcessSequence(), library.getId()); - - createExperimentElement( - root, - library, - study, - libraryConstructionProcess, - protocolToParameterMap, - bioSampleAccessions, - experimentId, - randomSubmissionIdentifier); - } - } - } - }); - } - })); + study -> { + if (study.getAssays() == null) { + return; + } + + study + .getAssays() + .forEach( + assay -> { + // Start from DataFiles + if (assay.getDataFiles() == null) { + return; + } + + assay + .getDataFiles() + .forEach( + dataFile -> + mapExperimentForDataFile( + root, + protocolToParameterNameMap, + bioSampleAccessions, + randomSubmissionIdentifier, + experimentSequence, + study, + assay, + dataFile)); + }); + }); return experimentSequence; } /** - * Finds a process that has the given output ID. Handles both #data_file/334 and #data/334 - * formats. + * Resolves the library associated with a data file and creates the corresponding ENA experiment + * once for that library. */ - private ProcessSequence findProcessByOutputId( - final List processSequence, final String outputId) { - if (processSequence == null || outputId == null) { - return null; + private void mapExperimentForDataFile( + final Element root, + final Map> protocolToParameterNameMap, + final Map bioSampleAccessions, + final String randomSubmissionIdentifier, + final Map experimentSequence, + final Study study, + final Assay assay, + final DataFile dataFile) { + final ProcessSequence sequencingProcess = + IsaJsonGraphLookup.findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); + + if (sequencingProcess == null) { + return; } - // Normalize the outputId (handle both #data_file/334 and #data/334) - final String normalizedOutputId = normalizeDataFileId(outputId); + final OtherMaterial library = + IsaJsonGraphLookup.findOtherMaterialFromProcessInput( + sequencingProcess, assay.getMaterials()); - for (final ProcessSequence process : processSequence) { - if (process.getOutputs() != null) { - for (final Output output : process.getOutputs()) { - if (output.getId() != null) { - final String normalizedProcessOutputId = normalizeDataFileId(output.getId()); - if (normalizedProcessOutputId.equals(normalizedOutputId)) { - return process; - } - } - } + if (library == null || experimentSequence.containsKey(library.getId())) { + return; + } + + final String experimentId = library.getId() + "-" + randomSubmissionIdentifier; + experimentSequence.put(library.getId(), experimentId); + + final List materialLineage = + findMaterialLineageToSample(library, assay.getMaterials()); + final ProcessSequence libraryConstructionProcess = + IsaJsonGraphLookup.findProcessByOutputId(assay.getProcessSequence(), library.getId()); + final List experimentProcesses = + findExperimentProcesses( + assay.getProcessSequence(), + libraryConstructionProcess, + sequencingProcess, + materialLineage); + + createExperimentElement( + root, + study, + library, + materialLineage, + assay, + experimentProcesses, + protocolToParameterNameMap, + bioSampleAccessions, + experimentId, + randomSubmissionIdentifier); + } + + private List findExperimentProcesses( + final List assayProcesses, + final ProcessSequence libraryConstructionProcess, + final ProcessSequence sequencingProcess, + final List materialLineage) { + final List experimentProcesses = new ArrayList<>(); + final Set processIds = new HashSet<>(); + + addExperimentProcess(experimentProcesses, processIds, libraryConstructionProcess); + addExperimentProcess(experimentProcesses, processIds, sequencingProcess); + + if (materialLineage == null) { + return experimentProcesses; + } + + for (final OtherMaterial material : materialLineage) { + if (material == null || material.getId() == null) { + continue; } + + addExperimentProcess( + experimentProcesses, + processIds, + IsaJsonGraphLookup.findProcessByOutputId(assayProcesses, material.getId())); } - return null; + + return experimentProcesses; } - /** Normalizes data file IDs to handle both #data_file/334 and #data/334 formats. */ - private String normalizeDataFileId(final String id) { - if (id == null) { - return null; + private void addExperimentProcess( + final List experimentProcesses, + final Set processIds, + final ProcessSequence process) { + if (process == null || process.getId() == null || !processIds.add(process.getId())) { + return; } - // Convert #data_file/334 to #data/334 for comparison - return id.replace("#data_file/", "#data/"); + + experimentProcesses.add(process); } - /** Finds the Library (OtherMaterial) that was used as input to a process. */ - private OtherMaterial findLibraryFromProcessInput( - final ProcessSequence process, final Materials materials) { - if (process.getInputs() == null || materials == null || materials.getOtherMaterials() == null) { - return null; + /** + * Returns the library and upstream otherMaterials until the chain reaches a Study sample. + */ + private List findMaterialLineageToSample( + final OtherMaterial material, final Materials materials) { + final List materialLineage = new ArrayList<>(); + final Map otherMaterialsById = + IsaJsonGraphLookup.buildOtherMaterialsById(materials); + + collectMaterialLineage(material, otherMaterialsById, materialLineage, new HashSet<>()); + + return materialLineage; + } + + private void collectMaterialLineage( + final OtherMaterial material, + final Map otherMaterialsById, + final List materialLineage, + final Set visitedIds) { + if (material == null || material.getId() == null || !visitedIds.add(material.getId())) { + return; } - for (final Input input : process.getInputs()) { - if (input.getId() != null) { - for (final OtherMaterial otherMaterial : materials.getOtherMaterials()) { - if (otherMaterial.getId() != null && otherMaterial.getId().equals(input.getId())) { - return otherMaterial; - } - } + materialLineage.add(material); + + if (material.getDerivesFrom() == null) { + return; + } + + for (final DerivesFrom derivesFrom : material.getDerivesFrom()) { + if (derivesFrom == null || derivesFrom.getId() == null) { + continue; } + + collectMaterialLineage( + otherMaterialsById.get(derivesFrom.getId()), + otherMaterialsById, + materialLineage, + visitedIds); } - return null; } - /** Creates an ENA EXPERIMENT element from a Library (OtherMaterial). */ + /** Creates one ENA EXPERIMENT element using ENA-native assay/process parameter names. */ private void createExperimentElement( final Element root, - final OtherMaterial library, final Study study, - final ProcessSequence libraryConstructionProcess, - final Map> protocolToParameterMap, + final OtherMaterial library, + final List materialLineage, + final Assay assay, + final List experimentProcesses, + final Map> protocolToParameterNameMap, final Map bioSampleAccessions, final String experimentId, final String randomSubmissionIdentifier) { - final Element experimentElement = root.addElement("EXPERIMENT"); + // Process parameters are closest to ENA submission fields; material characteristics fill gaps. + final ExperimentMetadata experimentMetadata = + new ExperimentMetadata( + extractParameterValues(experimentProcesses, protocolToParameterNameMap), + extractMaterialCharacteristicValues( + materialLineage, buildCharacteristicKeyLookup(study, assay))); + final Element experimentElement = root.addElement("EXPERIMENT"); experimentElement.addAttribute("alias", experimentId); - experimentElement.addElement("TITLE").addText(library.getName()); + experimentElement + .addElement("TITLE") + .addText( + firstNonBlank( + experimentMetadata.get("EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), + library.getName())); experimentElement .addElement("STUDY_REF") - .addAttribute("refname", study.getTitle() + "-" + randomSubmissionIdentifier); + .addAttribute("refname", assay.getId() + "-" + randomSubmissionIdentifier); final Element designElement = experimentElement.addElement("DESIGN"); - designElement.addElement("DESIGN_DESCRIPTION").addText("ISA-Test"); + designElement + .addElement("DESIGN_DESCRIPTION") + .addText(experimentMetadata.require("DESIGN_DESCRIPTION")); - final String sourceBioSampleAccession = bioSampleAccessions.get("SOURCE"); + final String sampleAccession = + resolveSampleAccessionForLibrary(study, assay, library, bioSampleAccessions); designElement .addElement("SAMPLE_DESCRIPTOR") - .addAttribute("accession", sourceBioSampleAccession); + .addAttribute("accession", requireValue(sampleAccession, "BioSamples sample accession")); final Element libraryDescriptorElement = designElement.addElement("LIBRARY_DESCRIPTOR"); + addLibraryDescriptor(libraryDescriptorElement, library, experimentMetadata); + addPlatform(experimentElement, experimentMetadata); + } - // Extract library parameters from the library construction process - if (libraryConstructionProcess != null - && libraryConstructionProcess.getExecutesProtocol() != null) { - final String protocolId = libraryConstructionProcess.getExecutesProtocol().getId(); - final List protocolParameters = protocolToParameterMap.get(protocolId); - final List parameterValues = libraryConstructionProcess.getParameterValues(); + /** + * Resolves the BioSamples accession for the biological sample that the library derives from. + */ + private String resolveSampleAccessionForLibrary( + final Study study, + final Assay assay, + final OtherMaterial library, + final Map bioSampleAccessions) { + if (study == null || study.getMaterials() == null || study.getMaterials().getSamples() == null) { + return getBioSampleAccessionFallback(bioSampleAccessions, null); + } - if (protocolParameters != null && parameterValues != null) { - addLibraryParameters( - libraryDescriptorElement, library, protocolParameters, parameterValues); + final Map samplesById = new HashMap<>(); + for (final Sample sample : study.getMaterials().getSamples()) { + if (sample != null && sample.getId() != null) { + samplesById.put(sample.getId(), sample); } } - // Add platform information (hardcoded for now, could be extracted from sequencing process) - final Element platformElement = experimentElement.addElement("PLATFORM"); - final Element experimentTypeElement = platformElement.addElement("OXFORD_NANOPORE"); - experimentTypeElement.addElement("INSTRUMENT_MODEL").addText("MinION"); + final Map otherMaterialsById = + IsaJsonGraphLookup.buildOtherMaterialsById(assay.getMaterials()); + + final Sample sample = + findSampleForMaterialId(library.getId(), samplesById, otherMaterialsById, new HashSet<>()); + if (sample == null) { + return getBioSampleAccessionFallback(bioSampleAccessions, null); + } + + final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study, assay); + return firstNonBlank( + getCharacteristicAnnotation(sample.getCharacteristics(), characteristicKeyLookup), + getBioSampleAccessionFallback(bioSampleAccessions, sample)); + } + + private String getBioSampleAccessionFallback( + final Map bioSampleAccessions, final Sample sample) { + if (bioSampleAccessions == null || bioSampleAccessions.isEmpty()) { + return ""; + } + + if (sample != null) { + final String sampleAccession = + firstNonBlank( + getBioSampleAccessionByKey(bioSampleAccessions, sample.getId()), + getBioSampleAccessionByKey(bioSampleAccessions, sample.getName())); + if (sampleAccession != null) { + return sampleAccession; + } + } + + return firstNonBlank(bioSampleAccessions.get("SAMPLE"), bioSampleAccessions.get("SOURCE")); + } + + private String getBioSampleAccessionByKey( + final Map bioSampleAccessions, final String key) { + return key == null ? null : bioSampleAccessions.get(key); + } + + private Sample findSampleForMaterialId( + final String materialId, + final Map samplesById, + final Map otherMaterialsById, + final Set visitedIds) { + if (materialId == null || !visitedIds.add(materialId)) { + return null; + } + + final Sample sample = samplesById.get(materialId); + if (sample != null) { + return sample; + } + + final OtherMaterial otherMaterial = otherMaterialsById.get(materialId); + if (otherMaterial == null || otherMaterial.getDerivesFrom() == null) { + return null; + } + + for (final DerivesFrom derivesFrom : otherMaterial.getDerivesFrom()) { + if (derivesFrom == null || derivesFrom.getId() == null) { + continue; + } + + final Sample derivedSample = + findSampleForMaterialId( + derivesFrom.getId(), samplesById, otherMaterialsById, visitedIds); + if (derivedSample != null) { + return derivedSample; + } + } + + return null; + } + + private Map buildCharacteristicKeyLookup(final Study study, final Assay assay) { + final Map keyLookup = new HashMap<>(); + + if (study != null) { + addCharacteristicCategories(keyLookup, study.characteristicCategories); + } + + if (assay != null) { + addCharacteristicCategories(keyLookup, assay.characteristicCategories); + } + + return keyLookup; + } + + private void addCharacteristicCategories( + final Map keyLookup, + final List characteristicCategories) { + if (characteristicCategories == null) { + return; + } + + for (CharacteristicCategory characteristicCategory : characteristicCategories) { + if (characteristicCategory == null + || characteristicCategory.id == null + || characteristicCategory.characteristicType == null + || characteristicCategory.characteristicType.annotationValue == null + || characteristicCategory.characteristicType.annotationValue.isBlank()) { + continue; + } + + keyLookup.put( + characteristicCategory.id, + characteristicCategory.characteristicType.annotationValue); + } + } + + private String getCharacteristicAnnotation( + final List characteristics, final Map characteristicKeyLookup) { + if (characteristics == null) { + return ""; + } + + for (Characteristic characteristic : characteristics) { + if (characteristic.category == null) { + continue; + } + + final String characteristicName = + getCharacteristicName(characteristic.category, characteristicKeyLookup); + + if (metadataKeyMatches(characteristicName, "accession") && characteristic.value != null) { + return characteristic.value.annotationValue; + } + } + + return ""; + } + + private Map extractMaterialCharacteristicValues( + final List materialLineage, + final Map characteristicKeyLookup) { + final Map characteristicValuesByName = new HashMap<>(); + + if (materialLineage == null) { + return characteristicValuesByName; + } + + for (final OtherMaterial material : materialLineage) { + if (material == null || material.getCharacteristics() == null) { + continue; + } + + for (final Characteristic characteristic : material.getCharacteristics()) { + if (characteristic == null + || characteristic.getValue() == null + || characteristic.getValue().getAnnotationValue() == null) { + continue; + } + + final String characteristicName = + getCharacteristicName(characteristic.getCategory(), characteristicKeyLookup); + if (characteristicName == null || characteristicName.isBlank()) { + continue; + } + + characteristicValuesByName.putIfAbsent( + characteristicName, characteristic.getValue().getAnnotationValue()); + } + } + + return characteristicValuesByName; + } + + private String getCharacteristicName( + final Category category, final Map characteristicKeyLookup) { + if (category == null) { + return null; + } + + if (category.getId() != null) { + final String characteristicName = characteristicKeyLookup.get(category.getId()); + if (characteristicName != null && !characteristicName.isBlank()) { + return characteristicName; + } + } + + if (category.getCharacteristicType() != null + && category.getCharacteristicType().getAnnotationValue() != null + && !category.getCharacteristicType().getAnnotationValue().isBlank()) { + return category.getCharacteristicType().getAnnotationValue(); + } + + return characteristicCategoryIdToName(category.getId()); + } + + private String characteristicCategoryIdToName(final String characteristicCategoryId) { + if (characteristicCategoryId == null || characteristicCategoryId.isBlank()) { + return null; + } + + final String prefix = "#characteristic_category/"; + final String characteristicName = + characteristicCategoryId.startsWith(prefix) + ? characteristicCategoryId.substring(prefix.length()) + : characteristicCategoryId; + + return characteristicName.replaceFirst("_[0-9]+$", ""); } /** - * Adds library parameters to the library descriptor in the correct order: 1. LIBRARY_NAME 2. - * LIBRARY_STRATEGY 3. LIBRARY_SOURCE 4. LIBRARY_SELECTION 5. LIBRARY_LAYOUT + * Reads process parameter values by their declared ISA parameter names so ENA field names can be + * used directly in the ISA without an extra mapping layer. */ - private void addLibraryParameters( + private Map extractParameterValues( + final List processSequences, + final Map> protocolToParameterNameMap) { + final Map parameterValuesByName = new HashMap<>(); + + if (processSequences == null) { + return parameterValuesByName; + } + + for (final ProcessSequence processSequence : processSequences) { + extractParameterValues(processSequence, protocolToParameterNameMap) + .forEach(parameterValuesByName::putIfAbsent); + } + + return parameterValuesByName; + } + + private Map extractParameterValues( + final ProcessSequence processSequence, + final Map> protocolToParameterNameMap) { + final Map parameterValuesByName = new HashMap<>(); + + if (processSequence == null + || processSequence.getExecutesProtocol() == null + || processSequence.getExecutesProtocol().getId() == null + || processSequence.getParameterValues() == null) { + return parameterValuesByName; + } + + final Map parameterNamesById = + protocolToParameterNameMap.get(processSequence.getExecutesProtocol().getId()); + + if (parameterNamesById == null) { + return parameterValuesByName; + } + + for (ParameterValue parameterValue : processSequence.getParameterValues()) { + if (parameterValue == null + || parameterValue.getCategory() == null + || parameterValue.getCategory().getId() == null + || parameterValue.getValue() == null + || parameterValue.getValue().getAnnotationValue() == null + || parameterValue.getValue().getAnnotationValue().isBlank()) { + continue; + } + + final String parameterName = parameterNamesById.get(parameterValue.getCategory().getId()); + if (parameterName == null || parameterName.isBlank()) { + continue; + } + + parameterValuesByName.put(parameterName, parameterValue.getValue().getAnnotationValue()); + } + + return parameterValuesByName; + } + + /** Populates the ENA LIBRARY_DESCRIPTOR block in schema order. */ + private void addLibraryDescriptor( final Element libraryDescriptorElement, final OtherMaterial library, - final List protocolParameters, - final List parameterValues) { - // Collect parameter values first - String libraryName = library.getName() != null ? library.getName() : null; - String libraryStrategy = null; - String librarySource = null; - String librarySelection = null; - String libraryLayout = null; - - for (final Parameter parameter : protocolParameters) { - final String parameterId = parameter.getId(); - final String parameterName = parameter.getParameterName().getAnnotationValue(); - - for (final ParameterValue parameterValue : parameterValues) { - if (parameterValue.getCategory() != null - && parameterValue.getCategory().getId() != null - && parameterValue.getCategory().getId().equals(parameterId)) { - final String value = parameterValue.getValue().getAnnotationValue(); - - if (isALibraryStrategyParameterName(parameterName)) { - libraryStrategy = value; - } else if ("library source".equalsIgnoreCase(parameterName)) { - librarySource = value; - } else if ("library selection".equalsIgnoreCase(parameterName)) { - librarySelection = value; - } else if (isALibraryLayoutParameterName(parameterName)) { - libraryLayout = value; - } - } + final ExperimentMetadata experimentMetadata) { + addOptionalTextElement( + libraryDescriptorElement, + "LIBRARY_NAME", + firstNonBlank( + experimentMetadata.get("LIBRARY_NAME", "LIBRARY NAME"), + library.getName())); + libraryDescriptorElement + .addElement("LIBRARY_STRATEGY") + .addText(experimentMetadata.require("LIBRARY_STRATEGY")); + libraryDescriptorElement + .addElement("LIBRARY_SOURCE") + .addText(experimentMetadata.require("LIBRARY_SOURCE")); + libraryDescriptorElement + .addElement("LIBRARY_SELECTION") + .addText(experimentMetadata.require("LIBRARY_SELECTION")); + + final Element libraryLayoutElement = libraryDescriptorElement.addElement("LIBRARY_LAYOUT"); + final String layout = experimentMetadata.require("LIBRARY_LAYOUT"); + final Element layoutElement = libraryLayoutElement.addElement(layout); + + if ("PAIRED".equals(layout)) { + addOptionalAttribute( + layoutElement, "NOMINAL_LENGTH", experimentMetadata.get("NOMINAL_LENGTH")); + addOptionalAttribute( + layoutElement, "NOMINAL_SDEV", experimentMetadata.get("NOMINAL_SDEV")); + } + + addOptionalTextElement( + libraryDescriptorElement, + "POOLING_STRATEGY", + experimentMetadata.get("POOLING_STRATEGY")); + addOptionalTextElement( + libraryDescriptorElement, + "LIBRARY_CONSTRUCTION_PROTOCOL", + experimentMetadata.get("LIBRARY_CONSTRUCTION_PROTOCOL")); + } + + /** Populates the ENA PLATFORM block directly from sequencing-process parameters. */ + private void addPlatform( + final Element experimentElement, final ExperimentMetadata experimentMetadata) { + final String platform = experimentMetadata.require("PLATFORM"); + final String instrumentModel = experimentMetadata.require("INSTRUMENT_MODEL"); + + final Element platformElement = experimentElement.addElement("PLATFORM"); + final Element platformTypeElement = platformElement.addElement(platform); + platformTypeElement.addElement("INSTRUMENT_MODEL").addText(instrumentModel); + } + + private void addOptionalTextElement( + final Element parentElement, final String elementName, final String value) { + if (value != null && !value.isBlank()) { + parentElement.addElement(elementName).addText(value); + } + } + + private void addOptionalAttribute( + final Element element, final String attributeName, final String value) { + if (value != null && !value.isBlank()) { + element.addAttribute(attributeName, value); + } + } + + private String getMetadataValue(final Map metadata, final String... metadataNames) { + if (metadata == null || metadataNames == null) { + return null; + } + + for (final String metadataName : metadataNames) { + final String metadataValue = metadata.get(metadataName); + if (metadataValue != null && !metadataValue.isBlank()) { + return metadataValue; } } - // Add elements in the required order - // 1. LIBRARY_NAME - if (libraryName != null) { - libraryDescriptorElement.addElement("LIBRARY_NAME").addText(libraryName); + for (final String metadataName : metadataNames) { + for (final Map.Entry metadataEntry : metadata.entrySet()) { + if (metadataEntry.getValue() == null || metadataEntry.getValue().isBlank()) { + continue; + } + + if (metadataKeyMatches(metadataEntry.getKey(), metadataName)) { + return metadataEntry.getValue(); + } + } } - // 2. LIBRARY_STRATEGY - if (libraryStrategy != null) { - libraryDescriptorElement.addElement("LIBRARY_STRATEGY").addText(libraryStrategy); + return null; + } + + private boolean metadataKeyMatches(final String metadataKey, final String expectedMetadataKey) { + final String normalizedMetadataKey = normalizeMetadataKey(metadataKey); + return !normalizedMetadataKey.isBlank() + && normalizedMetadataKey.equals(normalizeMetadataKey(expectedMetadataKey)); + } + + private String normalizeMetadataKey(final String metadataKey) { + if (metadataKey == null) { + return ""; } - // 3. LIBRARY_SOURCE - if (librarySource != null) { - libraryDescriptorElement.addElement("LIBRARY_SOURCE").addText(librarySource); + return metadataKey.replaceAll("[^A-Za-z0-9]", "").toLowerCase(Locale.ROOT); + } + + private class ExperimentMetadata { + private final Map processMetadata; + private final Map materialMetadata; + + private ExperimentMetadata( + final Map processMetadata, final Map materialMetadata) { + this.processMetadata = processMetadata; + this.materialMetadata = materialMetadata; } - // 4. LIBRARY_SELECTION - if (librarySelection != null) { - libraryDescriptorElement.addElement("LIBRARY_SELECTION").addText(librarySelection); + private String get(final String... metadataNames) { + return firstNonBlank( + getMetadataValue(processMetadata, metadataNames), + getMetadataValue(materialMetadata, metadataNames)); } - // 5. LIBRARY_LAYOUT - if (libraryLayout != null) { - final Element libraryLayoutElement = libraryDescriptorElement.addElement("LIBRARY_LAYOUT"); - libraryLayoutElement.addElement(libraryLayout.toUpperCase()); + private String require(final String metadataName) { + return requireValue(get(metadataName), "metadata " + metadataName); } } - private boolean isALibraryStrategyParameterName(final String parameterName) { - return parameterName.equalsIgnoreCase("library strategy"); + private String requireValue(final String value, final String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException( + "Missing required ENA experiment " + fieldName + "."); + } + return value; } - private boolean isALibraryLayoutParameterName(final String parameterName) { - return parameterName.equalsIgnoreCase("library layout"); + private String firstNonBlank(final String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; } } diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinProjectXmlCreator.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinProjectXmlCreator.java deleted file mode 100644 index e92be9a6..00000000 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinProjectXmlCreator.java +++ /dev/null @@ -1,50 +0,0 @@ -/** Elixir BioHackathon 2022 */ -package com.elixir.biohackaton.ISAToSRA.sra.service; - -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Investigation; -import org.dom4j.Element; -import org.springframework.stereotype.Service; - -/** - * Service for creating ENA PROJECT XML elements from ISA-JSON Investigation objects. - * - *

    Converts ISA-JSON Investigation to ENA PROJECT_SET XML structure. The Investigation (top-level - * ISA-JSON element) is mapped to an ENA PROJECT element with: - * - *

      - *
    • TITLE: from Investigation.title - *
    • DESCRIPTION: from Investigation.description - *
    • SUBMISSION_PROJECT: contains SEQUENCING_PROJECT (hardcoded project type) - *
    - */ -@Service -public class WebinProjectXmlCreator { - /** - * Creates ENA PROJECT_SET XML element from ISA-JSON Investigation. - * - *

    Maps the Investigation to an ENA PROJECT element within a PROJECT_SET. The alias is - * generated using the investigation title and a random submission identifier to ensure - * uniqueness. - * - * @param webinElement the parent WEBIN element to add PROJECT_SET to - * @param investigation the Investigation object from ISA-JSON - * @param randomSubmissionIdentifier unique identifier for this submission - */ - public void createENAProjectSetElement( - final Element webinElement, - final Investigation investigation, - final String randomSubmissionIdentifier) { - final Element projectSetElement = webinElement.addElement("PROJECT_SET"); - final Element projectElement = projectSetElement.addElement("PROJECT"); - - // Create PROJECT element with alias based on investigation title and submission identifier - projectElement.addAttribute( - "alias", investigation.getTitle() + "-" + randomSubmissionIdentifier); - projectElement.addElement("TITLE").addText(investigation.getTitle()); - projectElement.addElement("DESCRIPTION").addText(investigation.getDescription()); - - // ENA requires SUBMISSION_PROJECT with a project type (e.g., SEQUENCING_PROJECT) - final Element submissionProjectElement = projectElement.addElement("SUBMISSION_PROJECT"); - submissionProjectElement.addElement("SEQUENCING_PROJECT"); - } -} diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinRunXmlCreator.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinRunXmlCreator.java index 8c2f93e3..2a193eb8 100644 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinRunXmlCreator.java +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinRunXmlCreator.java @@ -2,8 +2,10 @@ package com.elixir.biohackaton.ISAToSRA.sra.service; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.dom4j.Element; import org.springframework.stereotype.Service; @@ -23,6 +25,7 @@ public void createENARunSetElement( .getAssays() .forEach( assay -> { + final Set processedSequencingProcesses = new HashSet<>(); if (assay.getDataFiles() != null) { assay .getDataFiles() @@ -30,23 +33,31 @@ public void createENARunSetElement( dataFile -> { // Find the process that produced this data file final ProcessSequence sequencingProcess = - findProcessByOutputId( + IsaJsonGraphLookup.findProcessByOutputId( assay.getProcessSequence(), dataFile.getId()); if (sequencingProcess != null) { + if (!processedSequencingProcesses.add( + sequencingProcess.getId())) { + return; + } + // Find the library (experiment) that was input to sequencing final OtherMaterial library = - findLibraryFromProcessInput( + IsaJsonGraphLookup.findOtherMaterialFromProcessInput( sequencingProcess, assay.getMaterials()); if (library != null && experimentSequenceMap.containsKey(library.getId())) { final String experimentId = experimentSequenceMap.get(library.getId()); + final List runDataFiles = + IsaJsonGraphLookup.findDataFilesFromProcessOutputs( + sequencingProcess, assay.getDataFiles()); createRunElement( runSetElement, - dataFile, - assay, + sequencingProcess, + runDataFiles, experimentId, randomSubmissionIdentifier); } @@ -56,110 +67,64 @@ public void createENARunSetElement( })); } - /** - * Finds a process that has the given output ID. Handles both #data_file/334 and #data/334 - * formats. - */ - private ProcessSequence findProcessByOutputId( - final List processSequence, final String outputId) { - if (processSequence == null || outputId == null) { - return null; - } - - // Normalize the outputId (handle both #data_file/334 and #data/334) - final String normalizedOutputId = normalizeDataFileId(outputId); - - for (final ProcessSequence process : processSequence) { - if (process.getOutputs() != null) { - for (final Output output : process.getOutputs()) { - if (output.getId() != null) { - final String normalizedProcessOutputId = normalizeDataFileId(output.getId()); - if (normalizedProcessOutputId.equals(normalizedOutputId)) { - return process; - } - } - } - } - } - return null; - } - - /** Normalizes data file IDs to handle both #data_file/334 and #data/334 formats. */ - private String normalizeDataFileId(final String id) { - if (id == null) { - return null; - } - // Convert #data_file/334 to #data/334 for comparison - return id.replace("#data_file/", "#data/"); - } - - /** Finds the Library (OtherMaterial) that was used as input to a process. */ - private OtherMaterial findLibraryFromProcessInput( - final ProcessSequence process, final Materials materials) { - if (process.getInputs() == null || materials == null || materials.getOtherMaterials() == null) { - return null; - } - - for (final Input input : process.getInputs()) { - if (input.getId() != null) { - for (final OtherMaterial otherMaterial : materials.getOtherMaterials()) { - if (otherMaterial.getId() != null && otherMaterial.getId().equals(input.getId())) { - return otherMaterial; - } - } - } - } - return null; - } - - /** Creates an ENA RUN element from a DataFile. */ + /** Creates one ENA RUN element from all data files produced by a sequencing process. */ private void createRunElement( final Element runSetElement, - final DataFile dataFile, - final Assay assay, + final ProcessSequence sequencingProcess, + final List dataFiles, final String experimentId, final String randomSubmissionIdentifier) { final Element runElement = runSetElement .addElement("RUN") - .addAttribute("alias", dataFile.getId() + "-" + randomSubmissionIdentifier); + .addAttribute("alias", sequencingProcess.getId() + "-" + randomSubmissionIdentifier); - runElement.addElement("TITLE").addText(dataFile.getName() != null ? dataFile.getName() : ""); + final String runTitle = + !dataFiles.isEmpty() && dataFiles.get(0).getName() != null ? dataFiles.get(0).getName() : ""; + runElement.addElement("TITLE").addText(runTitle); runElement.addElement("EXPERIMENT_REF").addAttribute("refname", experimentId); - // Extract file metadata from comments - final String fileName = dataFile.getName(); - String fileType = null; - String checksum = null; + if (dataFiles.isEmpty()) { + throw new RuntimeException( + "Run file(s) not found or missing required metadata for sequencing process " + + sequencingProcess.getId()); + } - if (dataFile.getComments() != null) { - for (final Comment comment : dataFile.getComments()) { - if ("file type".equals(comment.getName())) { - fileType = comment.getValue() != null ? comment.getValue() : null; - } - if ("file checksum".equals(comment.getName())) { - checksum = comment.getValue() != null ? comment.getValue() : null; + final Element dataBlockElement = runElement.addElement("DATA_BLOCK"); + final Element filesElement = dataBlockElement.addElement("FILES"); + + for (final DataFile dataFile : dataFiles) { + final String fileName = dataFile.getName(); + String fileType = null; + String checksum = null; + + if (dataFile.getComments() != null) { + for (final Comment comment : dataFile.getComments()) { + if ("file type".equals(comment.getName())) { + fileType = comment.getValue() != null ? comment.getValue() : null; + } + if ("file checksum".equals(comment.getName())) { + checksum = comment.getValue() != null ? comment.getValue() : null; + } } } - } - if (fileName != null && fileType != null && checksum != null) { - final Element dataBlockElement = runElement.addElement("DATA_BLOCK"); - final Element filesElement = dataBlockElement.addElement("FILES"); + if (fileName == null || fileType == null || checksum == null) { + throw new RuntimeException( + "Run file(s) not found or missing required metadata: fileName=" + + fileName + + ", fileType=" + + fileType + + ", checksum=" + + checksum); + } + filesElement .addElement("FILE") .addAttribute("filename", fileName) .addAttribute("filetype", fileType) .addAttribute("checksum_method", "MD5") .addAttribute("checksum", checksum); - } else { - throw new RuntimeException( - "Run file(s) not found or missing required metadata: fileName=" - + fileName - + ", fileType=" - + fileType - + ", checksum=" - + checksum); } } } diff --git a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinStudyXmlCreator.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinStudyXmlCreator.java index d94ab869..836ebff2 100644 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinStudyXmlCreator.java +++ b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinStudyXmlCreator.java @@ -1,39 +1,36 @@ /** Elixir BioHackathon 2022 */ package com.elixir.biohackaton.ISAToSRA.sra.service; -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Comment; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.dom4j.Element; import org.springframework.stereotype.Service; /** - * Service for creating ENA STUDY XML elements from ISA-JSON Study objects. + * Service for creating ENA STUDY XML elements from ISA-JSON. * - *

    Converts ISA-JSON Study objects to ENA STUDY_SET XML structure. Each Study from the ISA-JSON - * is mapped to an ENA STUDY element with: - * - *

      - *
    • STUDY_TITLE: from Study.title - *
    • STUDY_DESCRIPTION: from Study.description - *
    • STUDY_ABSTRACT: from Study.description (same as description) - *
    • STUDY_TYPE: hardcoded to "Other" - *
    • STUDY_ATTRIBUTES: from Study.comments - *
    + *

    Each assay is treated as the ENA study unit so the ENA study alias can be mapped back to the + * assay path in the MARS receipt. Core study descriptor values come from the ISA Study itself, + * while ENA-specific study fields such as STUDY_ABSTRACT, STUDY_TYPE, and new_study_type are read + * from assay comments. */ @Service @Slf4j public class WebinStudyXmlCreator { - /** - * Creates ENA STUDY_SET XML element from ISA-JSON Study objects. - * - *

    Maps each Study to an ENA STUDY element within a STUDY_SET. The alias is generated using the - * study title and a random submission identifier to ensure uniqueness. - * - * @param webinElement the parent WEBIN element to add STUDY_SET to - * @param studies list of Study objects from ISA-JSON - * @param randomSubmissionIdentifier unique identifier for this submission - */ + private static final Set RESERVED_ASSAY_COMMENT_NAMES = + Set.of( + "target_repository", + "STUDY_ABSTRACT", + "STUDY_TYPE", + "existing_study_type", + "new_study_type"); + public void createENAStudySetElement( final Element webinElement, final List studies, @@ -43,44 +40,161 @@ public void createENAStudySetElement( studies.forEach( study -> { - // Create STUDY element with alias based on title and submission identifier - final Element studyElement = - studySetElement - .addElement("STUDY") - .addAttribute("alias", study.getTitle() + "-" + randomSubmissionIdentifier); - - // Create DESCRIPTOR element containing study metadata - final Element studyDescriptorElement = studyElement.addElement("DESCRIPTOR"); - - studyDescriptorElement.addElement("STUDY_TITLE").addText(study.getTitle()); - studyDescriptorElement.addElement("STUDY_DESCRIPTION").addText(study.getDescription()); - // ENA requires both STUDY_DESCRIPTION and STUDY_ABSTRACT - studyDescriptorElement.addElement("STUDY_ABSTRACT").addText(study.getDescription()); - studyDescriptorElement - .addElement("STUDY_TYPE") - .addAttribute("existing_study_type", "Other"); - - // Add study attributes from comments - final Element studyAttributesElement = studyElement.addElement("STUDY_ATTRIBUTES"); - - if (study.getComments() != null) { - study - .getComments() - .forEach( - comment -> { - final Element studyAttributeElement = - studyAttributesElement.addElement("STUDY_ATTRIBUTE"); - - studyAttributeElement.addElement("TAG").addText(comment.getName()); - studyAttributeElement - .addElement("VALUE") - .addText((String) comment.getValue()); - }); + if (study.getAssays() == null) { + return; } + + study + .getAssays() + .forEach( + assay -> createStudyElement( + studySetElement, study, assay, randomSubmissionIdentifier)); }); } catch (final Exception e) { log.error("Failed to parse ISA JSON and create ENA study", e); throw new RuntimeException("Failed to create ENA STUDY elements", e); } } + + private void createStudyElement( + final Element studySetElement, + final Study study, + final Assay assay, + final String randomSubmissionIdentifier) { + final Map assayCommentMap = buildCommentMap(assay.getComments()); + final String assayId = requireAssayField(assay.getId(), "assay @id"); + + final Element studyElement = + studySetElement + .addElement("STUDY") + .addAttribute("alias", assayId + "-" + randomSubmissionIdentifier); + + final Element studyDescriptorElement = studyElement.addElement("DESCRIPTOR"); + studyDescriptorElement + .addElement("STUDY_TITLE") + .addText(requireStudyField(study != null ? study.getTitle() : null, "study title", assayId)); + + addOptionalTextElement( + studyDescriptorElement, "STUDY_DESCRIPTION", study != null ? study.getDescription() : null); + addOptionalTextElement( + studyDescriptorElement, + "STUDY_ABSTRACT", + firstNonBlank(assayCommentMap.get("STUDY_ABSTRACT"), study != null ? study.getDescription() : null)); + + final Element studyTypeElement = studyDescriptorElement.addElement("STUDY_TYPE"); + studyTypeElement.addAttribute( + "existing_study_type", + requireAssayComment( + assayCommentMap, + firstNonBlankCommentName(assayCommentMap, "STUDY_TYPE", "existing_study_type"), + assayId)); + + final String newStudyType = assayCommentMap.get("new_study_type"); + if (newStudyType != null && !newStudyType.isBlank()) { + studyTypeElement.addAttribute("new_study_type", newStudyType); + } + + final Element studyAttributesElement = studyElement.addElement("STUDY_ATTRIBUTES"); + addCommentsAsStudyAttributes(studyAttributesElement, study.getComments(), Set.of()); + addCommentsAsStudyAttributes( + studyAttributesElement, assay.getComments(), RESERVED_ASSAY_COMMENT_NAMES); + } + + private Map buildCommentMap(final List comments) { + final Map commentMap = new HashMap<>(); + + if (comments == null) { + return commentMap; + } + + for (Comment comment : comments) { + if (comment == null || comment.getName() == null || comment.getValue() == null) { + continue; + } + + commentMap.put(comment.getName(), String.valueOf(comment.getValue())); + } + + return commentMap; + } + + private void addCommentsAsStudyAttributes( + final Element studyAttributesElement, + final List comments, + final Set namesToSkip) { + if (comments == null) { + return; + } + + comments.forEach( + comment -> { + if (comment == null + || comment.getName() == null + || comment.getValue() == null + || namesToSkip.contains(comment.getName())) { + return; + } + + final Element studyAttributeElement = + studyAttributesElement.addElement("STUDY_ATTRIBUTE"); + studyAttributeElement.addElement("TAG").addText(comment.getName()); + studyAttributeElement.addElement("VALUE").addText(String.valueOf(comment.getValue())); + }); + } + + private void addOptionalTextElement( + final Element parentElement, final String elementName, final String value) { + if (value != null && !value.isBlank()) { + parentElement.addElement(elementName).addText(value); + } + } + + private String requireStudyField( + final String value, final String fieldName, final String assayId) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException( + "Assay " + assayId + " cannot create an ENA STUDY because the " + fieldName + " is missing."); + } + return value; + } + + private String requireAssayComment( + final Map assayCommentMap, + final String commentName, + final String assayId) { + final String value = assayCommentMap.get(commentName); + if (value == null || value.isBlank()) { + throw new IllegalArgumentException( + "Assay " + assayId + " is missing required ENA study comment " + commentName + "."); + } + return value; + } + + private String requireAssayField(final String value, final String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException( + "Cannot create ENA STUDY element because the " + fieldName + " is missing."); + } + return value; + } + + private String firstNonBlankCommentName( + final Map assayCommentMap, final String... commentNames) { + for (String commentName : commentNames) { + final String value = assayCommentMap.get(commentName); + if (value != null && !value.isBlank()) { + return commentName; + } + } + return commentNames[0]; + } + + private String firstNonBlank(final String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } } diff --git a/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinExperimentXmlCreatorTest.java b/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinExperimentXmlCreatorTest.java index 188da62d..3884df41 100644 --- a/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinExperimentXmlCreatorTest.java +++ b/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinExperimentXmlCreatorTest.java @@ -25,6 +25,7 @@ class WebinExperimentXmlCreatorTest { private WebinExperimentXmlCreator experimentXmlCreator; private ObjectMapper objectMapper; private IsaJson isaJson; + private IsaJson multiIsaJson; @BeforeEach void setUp() throws Exception { @@ -35,6 +36,10 @@ void setUp() throws Exception { String isaJsonFilePath = "../../test-data/biosamples-input-isa.json"; String isaJsonFile = Files.readString(new File(isaJsonFilePath).toPath()); isaJson = objectMapper.readValue(isaJsonFile, IsaJson.class); + + String multiIsaJsonFilePath = "../../test-data/biosamples-input-isa-multi.json"; + String multiIsaJsonFile = Files.readString(new File(multiIsaJsonFilePath).toPath()); + multiIsaJson = objectMapper.readValue(multiIsaJsonFile, IsaJson.class); } @Test @@ -75,13 +80,20 @@ void testCreateENAExperimentSetElement() throws Exception { // Verify TITLE element final Element titleElement = firstExperiment.element("TITLE"); Assertions.assertNotNull(titleElement, "EXPERIMENT should have a TITLE element"); - Assertions.assertFalse(titleElement.getText().isEmpty(), "TITLE should not be empty"); + Assertions.assertEquals( + "Arabidopsis leaf amplicon sequencing experiment", + titleElement.getText(), + "TITLE should come from the upstream other-material Title characteristic"); // Verify STUDY_REF element final Element studyRef = firstExperiment.element("STUDY_REF"); Assertions.assertNotNull(studyRef, "EXPERIMENT should have a STUDY_REF element"); Assertions.assertNotNull( studyRef.attribute("refname"), "STUDY_REF should have a refname attribute"); + Assertions.assertEquals( + "#assay/18_20_21-test-123", + studyRef.attributeValue("refname"), + "STUDY_REF should point at the assay-backed ENA study alias"); // Verify DESIGN element final Element design = firstExperiment.element("DESIGN"); @@ -90,6 +102,10 @@ void testCreateENAExperimentSetElement() throws Exception { // Verify DESIGN_DESCRIPTION final Element designDescription = design.element("DESIGN_DESCRIPTION"); Assertions.assertNotNull(designDescription, "DESIGN should have a DESIGN_DESCRIPTION element"); + Assertions.assertEquals( + "Amplicon sequencing of Arabidopsis thaliana leaf DNA.", + designDescription.getText(), + "DESIGN_DESCRIPTION should come from the ENA-native assay parameter"); // Verify SAMPLE_DESCRIPTOR final Element sampleDescriptor = design.element("SAMPLE_DESCRIPTOR"); @@ -102,6 +118,22 @@ void testCreateENAExperimentSetElement() throws Exception { // Verify LIBRARY_DESCRIPTOR final Element libraryDescriptor = design.element("LIBRARY_DESCRIPTOR"); Assertions.assertNotNull(libraryDescriptor, "DESIGN should have a LIBRARY_DESCRIPTOR element"); + Assertions.assertEquals( + "arabidopsis_leaf_amplicon_library", + libraryDescriptor.elementText("LIBRARY_NAME"), + "LIBRARY_NAME should come from the ENA-native assay parameter"); + Assertions.assertEquals( + "AMPLICON", + libraryDescriptor.elementText("LIBRARY_STRATEGY"), + "LIBRARY_STRATEGY should come from the ENA-native assay parameter"); + Assertions.assertEquals( + "GENOMIC", + libraryDescriptor.elementText("LIBRARY_SOURCE"), + "LIBRARY_SOURCE should come from the ENA-native assay parameter"); + Assertions.assertEquals( + "PCR", + libraryDescriptor.elementText("LIBRARY_SELECTION"), + "LIBRARY_SELECTION should come from the ENA-native assay parameter"); // Verify PLATFORM element final Element platform = firstExperiment.element("PLATFORM"); @@ -115,6 +147,10 @@ void testCreateENAExperimentSetElement() throws Exception { final Element instrumentModel = oxfordNanopore.element("INSTRUMENT_MODEL"); Assertions.assertNotNull( instrumentModel, "OXFORD_NANOPORE should have an INSTRUMENT_MODEL element"); + Assertions.assertEquals( + "MinION", + instrumentModel.getText(), + "INSTRUMENT_MODEL should come from the ENA-native sequencing parameter"); // Print XML for debugging (optional) final OutputFormat format = OutputFormat.createPrettyPrint(); @@ -126,12 +162,120 @@ void testCreateENAExperimentSetElement() throws Exception { } @Test - void testCreateENAExperimentSetElementWithMultipleDataFiles() throws Exception { - // This test verifies that multiple data files from the same library - // only create one experiment (deduplication) + void testExperimentTitleCanComeFromProcessMetadata() throws Exception { final Document document = DocumentHelper.createDocument(); final Element webinElement = document.addElement("WEBIN"); final List studies = isaJson.getInvestigation().getStudies(); + final String randomSubmissionIdentifier = "test-process-title"; + final Map bioSampleAccessions = new HashMap<>(); + bioSampleAccessions.put("SOURCE", "SAMEA130793922"); + + final Parameter titleParameter = new Parameter(); + titleParameter.setId("#parameter/experiment_title_test"); + final ParameterName titleParameterName = new ParameterName(); + titleParameterName.setAnnotationValue("Experiment Title"); + titleParameter.setParameterName(titleParameterName); + + final Protocol libraryProtocol = + studies.get(0).getProtocols().stream() + .filter(protocol -> "#protocol/20_20".equals(protocol.getId())) + .findFirst() + .orElseThrow(); + libraryProtocol.getParameters().add(titleParameter); + + final ParameterValue titleParameterValue = new ParameterValue(); + final Category titleCategory = new Category(); + titleCategory.setId("#parameter/experiment_title_test"); + titleParameterValue.setCategory(titleCategory); + final Value titleValue = new Value(); + titleValue.setAnnotationValue("Process metadata experiment title"); + titleParameterValue.setValue(titleValue); + + final ProcessSequence libraryConstructionProcess = + studies.get(0).getAssays().get(0).getProcessSequence().stream() + .filter(process -> "#process/library_construction/333".equals(process.getId())) + .findFirst() + .orElseThrow(); + libraryConstructionProcess.getParameterValues().add(titleParameterValue); + + experimentXmlCreator.createENAExperimentSetElement( + bioSampleAccessions, webinElement, studies, randomSubmissionIdentifier); + + final Element experimentSet = webinElement.element("EXPERIMENT_SET"); + @SuppressWarnings("unchecked") + final List experiments = experimentSet.elements("EXPERIMENT"); + + Assertions.assertEquals( + "Process metadata experiment title", + experiments.get(0).elementText("TITLE"), + "TITLE should come from process metadata when present"); + } + + @Test + void testExperimentMetadataCanComeFromOtherMaterialCharacteristics() throws Exception { + final Document document = DocumentHelper.createDocument(); + final Element webinElement = document.addElement("WEBIN"); + final List studies = isaJson.getInvestigation().getStudies(); + final Assay assay = studies.get(0).getAssays().get(0); + final String randomSubmissionIdentifier = "test-material-metadata"; + final Map bioSampleAccessions = new HashMap<>(); + bioSampleAccessions.put("SOURCE", "SAMEA130793922"); + + final ProcessSequence libraryConstructionProcess = + assay.getProcessSequence().stream() + .filter(process -> "#process/library_construction/333".equals(process.getId())) + .findFirst() + .orElseThrow(); + libraryConstructionProcess + .getParameterValues() + .removeIf( + parameterValue -> + parameterValue.getCategory() != null + && "#parameter/353".equals(parameterValue.getCategory().getId())); + + final CharacteristicCategory strategyCategory = new CharacteristicCategory(); + strategyCategory.setId("#characteristic_category/library_strategy_test"); + final CharacteristicType strategyType = new CharacteristicType(); + strategyType.setAnnotationValue("LIBRARY_STRATEGY"); + strategyCategory.setCharacteristicType(strategyType); + assay.getCharacteristicCategories().add(strategyCategory); + + final OtherMaterial extract = + assay.getMaterials().getOtherMaterials().stream() + .filter(material -> "#other_material/332".equals(material.getId())) + .findFirst() + .orElseThrow(); + final Characteristic strategyCharacteristic = new Characteristic(); + final Category strategyCharacteristicCategory = new Category(); + strategyCharacteristicCategory.setId("#characteristic_category/library_strategy_test"); + strategyCharacteristic.setCategory(strategyCharacteristicCategory); + final Value strategyValue = new Value(); + strategyValue.setAnnotationValue("WGS"); + strategyCharacteristic.setValue(strategyValue); + extract.getCharacteristics().add(strategyCharacteristic); + + experimentXmlCreator.createENAExperimentSetElement( + bioSampleAccessions, webinElement, studies, randomSubmissionIdentifier); + + final Element experimentSet = webinElement.element("EXPERIMENT_SET"); + @SuppressWarnings("unchecked") + final List experiments = experimentSet.elements("EXPERIMENT"); + final Element libraryDescriptor = + experiments.get(0).element("DESIGN").element("LIBRARY_DESCRIPTOR"); + + Assertions.assertEquals( + "WGS", + libraryDescriptor.elementText("LIBRARY_STRATEGY"), + "LIBRARY_STRATEGY should come from other-material characteristics when process metadata is absent"); + } + + @Test + void testCreateENAExperimentSetElementWithMultipleDataFiles() throws Exception { + // Paired data files from one library should deduplicate to one experiment, while a second + // library in the same assay should still create its own experiment. + final Document document = DocumentHelper.createDocument(); + final Element webinElement = document.addElement("WEBIN"); + final List studies = multiIsaJson.getInvestigation().getStudies(); final String randomSubmissionIdentifier = "test-456"; final Map bioSampleAccessions = new HashMap<>(); bioSampleAccessions.put("SOURCE", "SAMEA130793922"); @@ -145,29 +289,20 @@ void testCreateENAExperimentSetElementWithMultipleDataFiles() throws Exception { final Element experimentSet = webinElement.element("EXPERIMENT_SET"); Assertions.assertNotNull(experimentSet); - // Count unique libraries in the ISA JSON - final long uniqueLibraries = - studies.stream() - .flatMap(study -> study.getAssays().stream()) - .flatMap( - assay -> - assay.getMaterials() != null && assay.getMaterials().getOtherMaterials() != null - ? assay.getMaterials().getOtherMaterials().stream() - : java.util.stream.Stream.empty()) - .filter( - material -> - WebinExperimentXmlCreator.OTHER_MATERIAL_LIBRARY_NAME_DETERMINES_EXPERIMENT - .equalsIgnoreCase(material.getType())) - .map(OtherMaterial::getId) - .distinct() - .count(); - // The number of experiments should match the number of unique libraries @SuppressWarnings("unchecked") final List experiments = experimentSet.elements("EXPERIMENT"); Assertions.assertEquals( - uniqueLibraries, + 2, experimentSequence.size(), "Expected two unique libraries in the multi ISA fixture"); + Assertions.assertEquals( + experimentSequence.size(), experiments.size(), - "Number of experiments should match number of unique libraries"); + "Number of experiments should match the resolved experiment sequence"); + Assertions.assertTrue( + experimentSequence.containsKey("#other_material/333"), + "Expected the paired-end data files to resolve to library 1"); + Assertions.assertTrue( + experimentSequence.containsKey("#other_material/338"), + "Expected the single-end data file to resolve to library 2"); } } diff --git a/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinRunXmlCreatorTest.java b/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinRunXmlCreatorTest.java index 2f230ffb..b98cc1a2 100644 --- a/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinRunXmlCreatorTest.java +++ b/repository-services/isajson-ena/src/test/java/com/elixir/biohackaton/ISAToSRA/WebinRunXmlCreatorTest.java @@ -27,6 +27,7 @@ class WebinRunXmlCreatorTest { private WebinExperimentXmlCreator experimentXmlCreator; private ObjectMapper objectMapper; private IsaJson isaJson; + private IsaJson multiIsaJson; @BeforeEach void setUp() throws Exception { @@ -38,6 +39,10 @@ void setUp() throws Exception { String isaJsonFilePath = "../../test-data/biosamples-input-isa.json"; String isaJsonFile = Files.readString(new File(isaJsonFilePath).toPath()); isaJson = objectMapper.readValue(isaJsonFile, IsaJson.class); + + String multiIsaJsonFilePath = "../../test-data/biosamples-input-isa-multi.json"; + String multiIsaJsonFile = Files.readString(new File(multiIsaJsonFilePath).toPath()); + multiIsaJson = objectMapper.readValue(multiIsaJsonFile, IsaJson.class); } @Test @@ -127,10 +132,11 @@ void testCreateENARunSetElement() throws Exception { @Test void testCreateENARunSetElementWithMultipleDataFiles() throws Exception { - // This test verifies that multiple data files create multiple runs + // This test verifies that paired files stay in one run while a separate + // single-end experiment creates its own run. final Document document = DocumentHelper.createDocument(); final Element webinElement = document.addElement("WEBIN"); - final List studies = isaJson.getInvestigation().getStudies(); + final List studies = multiIsaJson.getInvestigation().getStudies(); final String randomSubmissionIdentifier = "test-456"; final Map bioSampleAccessions = new HashMap<>(); bioSampleAccessions.put("SOURCE", "SAMEA130793922"); @@ -144,21 +150,43 @@ void testCreateENARunSetElementWithMultipleDataFiles() throws Exception { runXmlCreator.createENARunSetElement( webinElement, studies, experimentSequenceMap, randomSubmissionIdentifier); - // Count data files in the ISA JSON - final long dataFileCount = - studies.stream() - .flatMap(study -> study.getAssays().stream()) - .filter(assay -> assay.getDataFiles() != null) - .flatMap(assay -> assay.getDataFiles().stream()) - .count(); - - // Verify that we have runs matching the number of data files final Element runSet = webinElement.element("RUN_SET"); Assertions.assertNotNull(runSet); @SuppressWarnings("unchecked") final List runs = runSet.elements("RUN"); + Assertions.assertEquals(2, runs.size(), "Expected one paired run and one single-end run"); + + final Element pairedRun = + runs.stream() + .filter( + run -> + "#process/nucleic_acid_sequencing/334-test-456" + .equals(run.attributeValue("alias"))) + .findFirst() + .orElse(null); + Assertions.assertNotNull(pairedRun, "Expected a run for the paired sequencing process"); + + final Element pairedFiles = pairedRun.element("DATA_BLOCK").element("FILES"); + @SuppressWarnings("unchecked") + final List pairedFileElements = pairedFiles.elements("FILE"); + Assertions.assertEquals( + 2, pairedFileElements.size(), "Paired sequencing run should include two FASTQ files"); + + final Element singleRun = + runs.stream() + .filter( + run -> + "#process/nucleic_acid_sequencing/339-test-456" + .equals(run.attributeValue("alias"))) + .findFirst() + .orElse(null); + Assertions.assertNotNull(singleRun, "Expected a run for the single-end sequencing process"); + + final Element singleFiles = singleRun.element("DATA_BLOCK").element("FILES"); + @SuppressWarnings("unchecked") + final List singleFileElements = singleFiles.elements("FILE"); Assertions.assertEquals( - dataFileCount, runs.size(), "Number of RUN elements should match number of data files"); + 1, singleFileElements.size(), "Single-end sequencing run should contain one FASTQ file"); } } diff --git a/repository-services/receipt/src/main/java/com/elixir/biohackaton/ISAToSRA/receipt/MarsReceiptProvider.java b/repository-services/receipt/src/main/java/com/elixir/biohackaton/ISAToSRA/receipt/MarsReceiptProvider.java index 46329e0f..257ce61e 100644 --- a/repository-services/receipt/src/main/java/com/elixir/biohackaton/ISAToSRA/receipt/MarsReceiptProvider.java +++ b/repository-services/receipt/src/main/java/com/elixir/biohackaton/ISAToSRA/receipt/MarsReceiptProvider.java @@ -7,7 +7,9 @@ import java.util.Map.Entry; import java.util.Optional; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.IsaJson; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsAccession; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsError; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsErrorType; @@ -134,11 +136,28 @@ protected void setMarsAccessions( .forEach( study -> { if (studiesAccessionsMap != null) { - ReceiptAccessionMap studyAccessionMap = getAccessionMapEntry( - studiesAccessionsMap, study, marsMessage); - if (studyAccessionMap.accession != null) { - marsAccessions.add(getStudyMarsAccession(studyAccessionMap)); + if (Assay.Fields.id.equals(studiesAccessionsMap.isaItemName)) { + Optional.ofNullable(study.assays) + .orElse(new ArrayList<>()) + .forEach( + assay -> { + ReceiptAccessionMap assayAccessionMap = getAccessionMapEntry( + studiesAccessionsMap, assay, marsMessage); + if (assayAccessionMap.accession != null) { + marsAccessions.add(getAssayStudyMarsAccession( + Map.entry(Study.Fields.title, study.title), assayAccessionMap)); + } + }); + } else { + ReceiptAccessionMap studyAccessionMap = getAccessionMapEntry( + studiesAccessionsMap, study, marsMessage); + if (studyAccessionMap.accession != null) { + marsAccessions.add(getStudyMarsAccession(studyAccessionMap)); + } } + ReceiptAccessionMap studyAccessionMap = new ReceiptAccessionMap(); + studyAccessionMap.isaFieldKey = Study.Fields.title; + studyAccessionMap.isaFieldValue = study.title; if (samplesAccessionsMap != null && study.materials != null) { Optional.ofNullable(study.materials.samples) .orElse(new ArrayList<>()) @@ -222,6 +241,30 @@ public MarsPath[] getStudyMarsPath(final Entry isaStudyKeyValue) }; } + public MarsPath[] getAssayMarsPath( + final Entry isaStudyKeyValue, + final Entry isaAssayKeyValue) { + return new MarsPath[] { + MarsPath.builder().key("investigation").build(), + MarsPath.builder() + .key("studies") + .where( + MarsWhere.builder() + .key(isaStudyKeyValue.getKey()) + .value(isaStudyKeyValue.getValue()) + .build()) + .build(), + MarsPath.builder() + .key("assays") + .where( + MarsWhere.builder() + .key(isaAssayKeyValue.getKey()) + .value(isaAssayKeyValue.getValue()) + .build()) + .build() + }; + } + public MarsPath[] getSampleMarsPath( final Entry isaStudyKeyValue, final Entry isaSampleKeyValue) { @@ -350,6 +393,19 @@ protected MarsAccession getSampleMarsAccession( .build(); } + protected MarsAccession getAssayStudyMarsAccession( + final Entry isaStudyKeyValue, + final ReceiptAccessionMap assayAccessionMap) { + return MarsAccession.builder() + .path( + List.of( + getAssayMarsPath( + isaStudyKeyValue, + Map.entry(assayAccessionMap.isaFieldKey, assayAccessionMap.isaFieldValue)))) + .value(assayAccessionMap.accession) + .build(); + } + protected MarsAccession getSourceMarsAccession( final ReceiptAccessionMap studyAccessionMap, final ReceiptAccessionMap sourceAccessionMap) { diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json new file mode 100644 index 00000000..0f8c304c --- /dev/null +++ b/test-data/biosamples-input-isa-multi.json @@ -0,0 +1,1690 @@ +{ + "investigation": { + "identifier": "investigation1", + "title": "Bob's investigation project", + "description": "Arabidopsis thaliana sequencing project for MARS PoC validation.", + "submissionDate": "", + "publicReleaseDate": "", + "ontologySourceReferences": [], + "filename": "i_Bob's investigation.txt", + "comments": [ + { + "name": "ISAjson export time", + "value": "2022-11-07T08:09:59Z" + }, + { + "name": "SEEK Project name", + "value": "Bob's PhD project" + }, + { + "name": "SEEK Project ID", + "value": "http://localhost:3000/single_pages/2" + }, + { + "name": "SEEK Investigation ID", + "value": "19" + } + ], + "publications": [], + "people": [ + { + "@id": "#people/5", + "lastName": "Bob", + "firstName": "Bob", + "midInitials": "", + "email": "bob@testing.com", + "phone": "", + "fax": "", + "address": "", + "affiliation": "", + "roles": [ + { + "termAccession": "", + "termSource": "", + "annotationValue": "" + } + ], + "comments": [ + { + "@id": "", + "value": "", + "name": "" + } + ] + } + ], + "studies": [ + { + "identifier": "study1", + "title": "Integrated multi-omics profiling of Arabidopsis thaliana under controlled experimental conditions", + "description": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions.\r\n", + "submissionDate": "", + "publicReleaseDate": "", + "filename": "s_Arabidopsis thaliana.txt", + "comments": [ + { + "name": "SEEK Study ID", + "value": "10" + }, + { + "name": "SEEK creation date", + "value": "2022-11-03T16:20:49Z" + } + ], + "publications": [], + "people": [ + { + "@id": "#people/5", + "lastName": "Bob", + "firstName": "Bob", + "midInitials": "", + "email": "bob@testing.com", + "phone": "", + "fax": "", + "address": "", + "affiliation": "", + "roles": [ + { + "termAccession": "", + "termSource": "", + "annotationValue": "" + } + ], + "comments": [ + { + "@id": "", + "value": "", + "name": "" + } + ] + } + ], + "studyDesignDescriptors": [], + "characteristicCategories": [ + { + "@id": "#characteristic_category/Title_317", + "characteristicType": { + "annotationValue": "Title", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/Description_318", + "characteristicType": { + "annotationValue": "Description", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/tax_id_319", + "characteristicType": { + "annotationValue": "tax_id", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/organism_320", + "characteristicType": { + "annotationValue": "organism", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/dev_stage_322", + "characteristicType": { + "annotationValue": "dev_stage", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/collection_date_323", + "characteristicType": { + "annotationValue": "collection date", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/isolation_source_324", + "characteristicType": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/collected_by_325", + "characteristicType": { + "annotationValue": "collected_by", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326", + "characteristicType": { + "annotationValue": "geographic location (country and/or sea)", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/submission_date_327", + "characteristicType": { + "annotationValue": "submission date", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/status_328", + "characteristicType": { + "annotationValue": "status", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/accession_329", + "characteristicType": { + "annotationValue": "accession", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/genotype_330", + "characteristicType": { + "annotationValue": "genotype", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/growth_condition_331", + "characteristicType": { + "annotationValue": "growth condition", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/organism_part_332", + "characteristicType": { + "annotationValue": "organism part", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/sample_description_333", + "characteristicType": { + "annotationValue": "sample description", + "termAccession": "", + "termSource": "" + } + } + ], + "materials": { + "sources": [ + { + "@id": "#source/330", + "name": "plant 1", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_317" + }, + "value": { + "annotationValue": "plant 1", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/Description_318" + }, + "value": { + "annotationValue": "plant in the lab", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/tax_id_319" + }, + "value": { + "annotationValue": "NCBI:txid3702", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/organism_320" + }, + "value": { + "annotationValue": "Arabidopsis thaliana", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/genotype_330" + }, + "value": { + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" + }, + "value": { + "annotationValue": "16 h light / 8 h dark growth chamber", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/dev_stage_322" + }, + "value": { + "annotationValue": "vegetative rosette stage", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/collection_date_323" + }, + "value": { + "annotationValue": "2022-01-01", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/isolation_source_324" + }, + "value": { + "annotationValue": "whole plant", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/collected_by_325" + }, + "value": { + "annotationValue": "Bob", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" + }, + "value": { + "annotationValue": "Belgium", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/submission_date_327" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/status_328" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/accession_329", + "characteristicType": { + "annotationValue": "accession", + "termAccession": "", + "termSource": "" + } + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ] + }, + { + "@id": "#source/430", + "name": "plant 2", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_317" + }, + "value": { + "annotationValue": "plant 2", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/Description_318" + }, + "value": { + "annotationValue": "greenhouse reference plant", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/tax_id_319" + }, + "value": { + "annotationValue": "NCBI:txid3702", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/organism_320" + }, + "value": { + "annotationValue": "Arabidopsis thaliana", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/genotype_330" + }, + "value": { + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" + }, + "value": { + "annotationValue": "greenhouse soil bench", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/dev_stage_322" + }, + "value": { + "annotationValue": "flowering stage", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/collection_date_323" + }, + "value": { + "annotationValue": "2022-01-03", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/isolation_source_324" + }, + "value": { + "annotationValue": "whole plant", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/collected_by_325" + }, + "value": { + "annotationValue": "Alice", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" + }, + "value": { + "annotationValue": "Belgium", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/submission_date_327" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/status_328" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/accession_329", + "characteristicType": { + "annotationValue": "accession", + "termAccession": "", + "termSource": "" + } + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ] + } + ], + "samples": [ + { + "@id": "#sample/331", + "name": "leaf 1", + "derivesFrom": [ + { + "@id": "#source/330" + } + ], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/organism_part_332" + }, + "value": { + "annotationValue": "leaf", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/sample_description_333" + }, + "value": { + "annotationValue": "young rosette leaf collected for DNA extraction", + "termSource": "", + "termAccession": "" + } + } + ], + "factorValues": [] + }, + { + "@id": "#sample/431", + "name": "root 1", + "derivesFrom": [ + { + "@id": "#source/430" + } + ], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/organism_part_332" + }, + "value": { + "annotationValue": "root", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/sample_description_333" + }, + "value": { + "annotationValue": "primary root segment collected for DNA extraction", + "termSource": "", + "termAccession": "" + } + } + ], + "factorValues": [] + } + ] + }, + "protocols": [ + { + "@id": "#protocol/18_10", + "name": "sample collection", + "protocolType": { + "annotationValue": "sample collection", + "termAccession": "", + "termSource": "" + }, + "description": "", + "uri": "", + "version": "", + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_method", + "parameterName": { + "annotationValue": "collection method", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_preservation", + "parameterName": { + "annotationValue": "sample preservation", + "termAccession": "", + "termSource": "" + } + } + ], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ] + }, + { + "@id": "#protocol/19_18", + "name": "nucleic acid extraction", + "protocolType": { + "annotationValue": "nucleic acid extraction", + "termAccession": "", + "termSource": "" + }, + "description": "", + "uri": "", + "version": "", + "parameters": [], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ] + }, + { + "@id": "#protocol/20_20", + "name": "library construction", + "protocolType": { + "annotationValue": "library construction", + "termAccession": "", + "termSource": "" + }, + "description": "", + "uri": "", + "version": "", + "parameters": [ + { + "@id": "#parameter/349", + "parameterName": { + "annotationValue": "LIBRARY_CONSTRUCTION_PROTOCOL", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/350", + "parameterName": { + "annotationValue": "LIBRARY_NAME", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/351", + "parameterName": { + "annotationValue": "DESIGN_DESCRIPTION", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/352", + "parameterName": { + "annotationValue": "LIBRARY_SOURCE", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/353", + "parameterName": { + "annotationValue": "LIBRARY_STRATEGY", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/354", + "parameterName": { + "annotationValue": "LIBRARY_SELECTION", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/355", + "parameterName": { + "annotationValue": "LIBRARY_LAYOUT", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/356", + "parameterName": { + "annotationValue": "NOMINAL_LENGTH", + "termAccession": "", + "termSource": "" + } + } + ], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ] + }, + { + "@id": "#protocol/21_21", + "name": "nucleic acid sequencing", + "protocolType": { + "annotationValue": "nucleic acid sequencing", + "termAccession": "", + "termSource": "" + }, + "description": "", + "uri": "", + "version": "", + "parameters": [ + { + "@id": "#parameter/362", + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/363", + "parameterName": { + "annotationValue": "INSTRUMENT_MODEL", + "termAccession": "", + "termSource": "" + } + } + ], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ] + } + ], + "processSequence": [ + { + "@id": "#process/sample_collection/331", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/18_10", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#source/330", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#sample/331", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "leaf tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_method" + }, + "value": { + "annotationValue": "sterile scalpel excision", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_preservation" + }, + "value": { + "annotationValue": "flash frozen in liquid nitrogen", + "termSource": "", + "termAccession": "" + } + } + ], + "nextProcess": { + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + }, + { + "@id": "#process/sample_collection/431", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/18_10", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#source/430", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#sample/431", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "root tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_method" + }, + "value": { + "annotationValue": "washed root excision", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_preservation" + }, + "value": { + "annotationValue": "flash frozen in liquid nitrogen", + "termSource": "", + "termAccession": "" + } + } + ], + "nextProcess": { + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + } + ], + "assays": [ + { + "@id": "#assay/18_20_21", + "filename": "a_assays.txt", + "measurementType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + }, + "technologyType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + }, + "technologyPlatform": "", + "characteristicCategories": [ + { + "@id": "#characteristic_category/Title_350", + "characteristicType": { + "annotationValue": "Title", + "termAccession": "", + "termSource": "" + } + } + ], + "comments": [ + { + "name": "target_repository", + "value": "ena" + }, + { + "name": "STUDY_ABSTRACT", + "value": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions." + }, + { + "name": "STUDY_TYPE", + "value": "Other" + }, + { + "name": "new_study_type", + "value": "Amplicon sequencing" + } + ], + "materials": { + "samples": [ + { + "@id": "#sample/331" + }, + { + "@id": "#sample/431" + } + ], + "otherMaterials": [ + { + "@id": "#other_material/332", + "name": "extract 1", + "type": "library name", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_350" + }, + "value": { + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", + "termAccession": "", + "termSource": "" + } + } + ], + "derivesFrom": [ + { + "@id": "#sample/331" + } + ], + "comments": [] + }, + { + "@id": "#other_material/333", + "name": "library 1", + "type": "library name", + "characteristics": [], + "derivesFrom": [ + { + "@id": "#other_material/332" + } + ], + "comments": [] + }, + { + "@id": "#other_material/337", + "name": "extract 2", + "type": "library name", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_350" + }, + "value": { + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment 2", + "termAccession": "", + "termSource": "" + } + } + ], + "derivesFrom": [ + { + "@id": "#sample/431" + } + ], + "comments": [] + }, + { + "@id": "#other_material/338", + "name": "library 2", + "type": "library name", + "characteristics": [], + "derivesFrom": [ + { + "@id": "#other_material/337" + } + ], + "comments": [] + } + ] + }, + "processSequence": [ + { + "@id": "#process/nucleic_acid_extraction/332", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/19_18", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#sample/331", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#other_material/332", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [], + "nextProcess": { + "@id": "#process/library_construction/333", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "@id": "#process/sample_collection/332", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + }, + { + "@id": "#process/library_construction/333", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/20_20", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#other_material/332", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#other_material/333", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [ + { + "category": { + "@id": "#parameter/349" + }, + "value": { + "annotationValue": "TruSeq DNA PCR-Free library preparation for Arabidopsis leaf DNA", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "unit": { + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/350" + }, + "value": { + "annotationValue": "arabidopsis_leaf_paired_library", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/351" + }, + "value": { + "annotationValue": "Paired-end genomic sequencing of Arabidopsis thaliana leaf DNA.", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "unit": { + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/352" + }, + "value": { + "annotationValue": "GENOMIC", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/353" + }, + "value": { + "annotationValue": "WGS", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/354" + }, + "value": { + "annotationValue": "RANDOM", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/355" + }, + "value": { + "annotationValue": "PAIRED", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/356" + }, + "value": { + "annotationValue": "350", + "termAccession": "", + "termSource": "", + "comments": [] + } + } + ], + "nextProcess": { + "@id": "#process/nucleic_acid_sequencing/334", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "@id": "#process/nucleic_acid_extraction/332", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + }, + { + "@id": "#process/nucleic_acid_sequencing/334", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/21_21", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#other_material/333", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#data_file/334", + "comments": [], + "characteristics": [], + "derivesFrom": [] + }, + { + "@id": "#data_file/335", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "ILLUMINA", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/363" + }, + "value": { + "annotationValue": "Illumina MiSeq", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "unit": { + "termAccession": "", + "termSource": "", + "comments": [] + } + } + ], + "nextProcess": { + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "@id": "#process/library_construction/333", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + }, + { + "@id": "#process/nucleic_acid_extraction/337", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/19_18", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#sample/431", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#other_material/337", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [], + "nextProcess": { + "@id": "#process/library_construction/338", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "@id": "#process/sample_collection/431", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + }, + { + "@id": "#process/library_construction/338", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/20_20", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#other_material/337", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#other_material/338", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [ + { + "category": { + "@id": "#parameter/349" + }, + "value": { + "annotationValue": "PCR amplicon library preparation for Arabidopsis root DNA", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "unit": { + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/350" + }, + "value": { + "annotationValue": "arabidopsis_root_amplicon_library", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/351" + }, + "value": { + "annotationValue": "Single-end amplicon sequencing of Arabidopsis thaliana root DNA.", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "unit": { + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/352" + }, + "value": { + "annotationValue": "GENOMIC", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/353" + }, + "value": { + "annotationValue": "AMPLICON", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/354" + }, + "value": { + "annotationValue": "PCR", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/355" + }, + "value": { + "annotationValue": "SINGLE", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/356" + }, + "value": { + "annotationValue": "150", + "termAccession": "", + "termSource": "", + "comments": [] + } + } + ], + "nextProcess": { + "@id": "#process/nucleic_acid_sequencing/339", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "@id": "#process/nucleic_acid_extraction/337", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + }, + { + "@id": "#process/nucleic_acid_sequencing/339", + "date": "", + "name": "", + "performer": "", + "executesProtocol": { + "@id": "#protocol/21_21", + "components": [], + "parameters": [] + }, + "inputs": [ + { + "@id": "#other_material/338", + "comments": [], + "characteristics": [] + } + ], + "outputs": [ + { + "@id": "#data_file/336", + "comments": [], + "characteristics": [], + "derivesFrom": [] + } + ], + "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "OXFORD_NANOPORE", + "termAccession": "", + "termSource": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/363" + }, + "value": { + "annotationValue": "MinION", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "unit": { + "termAccession": "", + "termSource": "", + "comments": [] + } + } + ], + "nextProcess": { + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "previousProcess": { + "@id": "#process/library_construction/338", + "inputs": [], + "outputs": [], + "parameterValues": [], + "comments": [] + }, + "comments": [] + } + ], + "dataFiles": [ + { + "@id": "#data/334", + "name": "ENA_TEST2.EXP1.R1.fastq.gz", + "type": "Raw Data File", + "comments": [ + { + "name": "file name", + "value": "ENA_TEST2.EXP1.R1.fastq.gz" + }, + { + "name": "file type", + "value": "fastq" + }, + { + "name": "file checksum", + "value": "" + }, + { + "name": "checksum_method", + "value": "MD5" + }, + { + "name": "accession", + "value": "" + }, + { + "name": "submission date", + "value": "" + } + ] + }, + { + "@id": "#data/335", + "name": "ENA_TEST2.EXP1.R2.fastq.gz", + "type": "Raw Data File", + "comments": [ + { + "name": "file name", + "value": "ENA_TEST2.EXP1.R2.fastq.gz" + }, + { + "name": "file type", + "value": "fastq" + }, + { + "name": "file checksum", + "value": "" + }, + { + "name": "checksum_method", + "value": "MD5" + }, + { + "name": "accession", + "value": "" + }, + { + "name": "submission date", + "value": "" + } + ] + }, + { + "@id": "#data/336", + "name": "ENA_TEST2.EXP2.SE.fastq.gz", + "type": "Raw Data File", + "comments": [ + { + "name": "file name", + "value": "ENA_TEST2.EXP2.SE.fastq.gz" + }, + { + "name": "file type", + "value": "fastq" + }, + { + "name": "file checksum", + "value": "" + }, + { + "name": "checksum_method", + "value": "MD5" + }, + { + "name": "accession", + "value": "" + }, + { + "name": "submission date", + "value": "" + } + ] + } + ], + "unitCategories": [] + } + ], + "factors": [], + "unitCategories": [] + } + ] + } +} diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index 8e938f2e..eeb2919c 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -56,8 +56,8 @@ "studies": [ { "identifier": "study1", - "title": "Arabidopsis thaliana", - "description": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis.\r\n", + "title": "Integrated multi-omics profiling of Arabidopsis thaliana under controlled experimental conditions", + "description": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions.\r\n", "submissionDate": "", "publicReleaseDate": "", "filename": "s_Arabidopsis thaliana.txt", @@ -133,14 +133,6 @@ "termSource": "" } }, - { - "@id": "#characteristic_category/cell_type_321", - "characteristicType": { - "annotationValue": "cell_type", - "termAccession": "", - "termSource": "" - } - }, { "@id": "#characteristic_category/dev_stage_322", "characteristicType": { @@ -204,6 +196,38 @@ "termAccession": "", "termSource": "" } + }, + { + "@id": "#characteristic_category/genotype_330", + "characteristicType": { + "annotationValue": "genotype", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/growth_condition_331", + "characteristicType": { + "annotationValue": "growth condition", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/organism_part_332", + "characteristicType": { + "annotationValue": "organism part", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/sample_description_333", + "characteristicType": { + "annotationValue": "sample description", + "termAccession": "", + "termSource": "" + } } ], "materials": { @@ -254,10 +278,20 @@ }, { "category": { - "@id": "#characteristic_category/cell_type_321" + "@id": "#characteristic_category/genotype_330" }, "value": { - "annotationValue": "na", + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" + }, + "value": { + "annotationValue": "16 h light / 8 h dark growth chamber", "termSource": "", "termAccession": "" } @@ -267,7 +301,7 @@ "@id": "#characteristic_category/dev_stage_322" }, "value": { - "annotationValue": "budding", + "annotationValue": "vegetative rosette stage", "termSource": "", "termAccession": "" } @@ -287,7 +321,7 @@ "@id": "#characteristic_category/isolation_source_324" }, "value": { - "annotationValue": "seed", + "annotationValue": "whole plant", "termSource": "", "termAccession": "" } @@ -359,7 +393,28 @@ "@id": "#source/330" } ], - "characteristics": [], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/organism_part_332" + }, + "value": { + "annotationValue": "leaf", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/sample_description_333" + }, + "value": { + "annotationValue": "young rosette leaf collected for DNA extraction", + "termSource": "", + "termAccession": "" + } + } + ], "factorValues": [] } ] @@ -376,7 +431,32 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_method", + "parameterName": { + "annotationValue": "collection method", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_preservation", + "parameterName": { + "annotationValue": "sample preservation", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -426,7 +506,15 @@ { "@id": "#parameter/349", "parameterName": { - "annotationValue": "library_construction_protocol", + "annotationValue": "LIBRARY_CONSTRUCTION_PROTOCOL", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/350", + "parameterName": { + "annotationValue": "LIBRARY_NAME", "termAccession": "", "termSource": "" } @@ -434,7 +522,7 @@ { "@id": "#parameter/351", "parameterName": { - "annotationValue": "design_description", + "annotationValue": "DESIGN_DESCRIPTION", "termAccession": "", "termSource": "" } @@ -442,7 +530,7 @@ { "@id": "#parameter/352", "parameterName": { - "annotationValue": "library source", + "annotationValue": "LIBRARY_SOURCE", "termAccession": "", "termSource": "" } @@ -450,7 +538,7 @@ { "@id": "#parameter/353", "parameterName": { - "annotationValue": "library strategy", + "annotationValue": "LIBRARY_STRATEGY", "termAccession": "", "termSource": "" } @@ -458,7 +546,7 @@ { "@id": "#parameter/354", "parameterName": { - "annotationValue": "library selection", + "annotationValue": "LIBRARY_SELECTION", "termAccession": "", "termSource": "" } @@ -466,7 +554,7 @@ { "@id": "#parameter/355", "parameterName": { - "annotationValue": "library layout", + "annotationValue": "LIBRARY_LAYOUT", "termAccession": "", "termSource": "" } @@ -474,7 +562,7 @@ { "@id": "#parameter/356", "parameterName": { - "annotationValue": "insert size", + "annotationValue": "NOMINAL_LENGTH", "termAccession": "", "termSource": "" } @@ -503,10 +591,18 @@ "uri": "", "version": "", "parameters": [ + { + "@id": "#parameter/362", + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + } + }, { "@id": "#parameter/363", "parameterName": { - "annotationValue": "sequencing instrument", + "annotationValue": "INSTRUMENT_MODEL", "termAccession": "", "termSource": "" } @@ -531,7 +627,38 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "leaf tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_method" + }, + "value": { + "annotationValue": "sterile scalpel excision", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_preservation" + }, + "value": { + "annotationValue": "flash frozen in liquid nitrogen", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, @@ -571,36 +698,24 @@ "termAccession": "", "termSource": "" } - }, - { - "@id": "#characteristic_category/submission_date_358", - "characteristicType": { - "annotationValue": "submission date", - "termAccession": "", - "termSource": "" - } - }, - { - "@id": "#characteristic_category/status_359", - "characteristicType": { - "annotationValue": "status", - "termAccession": "", - "termSource": "" - } - }, - { - "@id": "#characteristic_category/accession_360", - "characteristicType": { - "annotationValue": "accession", - "termAccession": "", - "termSource": "" - } } ], "comments": [ { "name": "target_repository", "value": "ena" + }, + { + "name": "STUDY_ABSTRACT", + "value": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions." + }, + { + "name": "STUDY_TYPE", + "value": "Other" + }, + { + "name": "new_study_type", + "value": "Amplicon sequencing" } ], "materials": { @@ -614,7 +729,18 @@ "@id": "#other_material/332", "name": "extract 1", "type": "library name", - "characteristics": [], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_350" + }, + "value": { + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", + "termAccession": "", + "termSource": "" + } + } + ], "derivesFrom": [ { "@id": "#sample/331" @@ -625,48 +751,7 @@ "@id": "#other_material/333", "name": "library 1", "type": "library name", - "characteristics": [ - { - "category": { - "@id": "#characteristic_category/Title_350" - }, - "value": { - "annotationValue": "library 1", - "termSource": "", - "termAccession": "" - } - }, - { - "category": { - "@id": "#characteristic_category/submission_date_358" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - }, - { - "category": { - "@id": "#characteristic_category/status_359" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - }, - { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - } - ], + "characteristics": [], "derivesFrom": [ { "@id": "#other_material/332" @@ -714,7 +799,7 @@ "@id": "#parameter/349" }, "value": { - "annotationValue": "lib prep", + "annotationValue": "PCR amplicon library preparation for Arabidopsis leaf DNA", "termSource": "", "termAccession": "" }, @@ -724,12 +809,22 @@ "comments": [] } }, + { + "category": { + "@id": "#parameter/350" + }, + "value": { + "annotationValue": "arabidopsis_leaf_amplicon_library", + "termSource": "", + "termAccession": "" + } + }, { "category": { "@id": "#parameter/351" }, "value": { - "annotationValue": "Test", + "annotationValue": "Amplicon sequencing of Arabidopsis thaliana leaf DNA.", "termSource": "", "termAccession": "" }, @@ -744,7 +839,7 @@ "@id": "#parameter/352" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "GENOMIC", "termSource": "", "termAccession": "" } @@ -754,7 +849,7 @@ "@id": "#parameter/353" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "AMPLICON", "termSource": "", "termAccession": "" } @@ -764,7 +859,7 @@ "@id": "#parameter/354" }, "value": { - "annotationValue": "RT-PCR", + "annotationValue": "PCR", "termSource": "", "termAccession": "" } @@ -816,12 +911,22 @@ "@id": "#protocol/21_21" }, "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "OXFORD_NANOPORE", + "termSource": "", + "termAccession": "" + } + }, { "category": { "@id": "#parameter/363" }, "value": { - "annotationValue": " MinION", + "annotationValue": "MinION", "termSource": "", "termAccession": "" }, diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index 4e322c2c..c4bdd180 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-isa.json @@ -1,79 +1,33 @@ { - "investigation": { - "identifier": "", - "title": "Bob's investigation", - "description": "", - "submissionDate": "", - "publicReleaseDate": "", - "ontologySourceReferences": [], - "filename": "i_Bob's investigation.txt", - "comments": [ - { - "name": "ISAjson export time", - "value": "2022-11-07T08:09:59Z" - }, - { - "name": "SEEK Project name", - "value": "Bob's PhD project" - }, - { - "name": "SEEK Project ID", - "value": "http://localhost:3000/single_pages/2" - }, - { - "name": "SEEK Investigation ID", - "value": "19" - } - ], - "publications": [], - "people": [ - { - "lastName": "Bob", - "firstName": "Bob", - "midInitials": "", - "email": "bob@testing.com", - "phone": "", - "fax": "", - "address": "", - "affiliation": "", - "roles": [ - { - "termAccession": "", - "termSource": "", - "annotationValue": "" - } - ], - "comments": [ - { - "name": "", - "value": "", - "@id": "" - } - ], - "@id": "#people/5" - } - ], - "studies": [ - { - "identifier": "", - "title": "Arabidopsis thaliana", - "description": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis.\r\n", - "submissionDate": "", - "publicReleaseDate": "", - "filename": "s_Arabidopsis thaliana.txt", - "comments": [ - { - "name": "SEEK Study ID", - "value": "10" - }, - { - "name": "SEEK creation date", - "value": "2022-11-03T16:20:49Z" - } - ], - "publications": [], - "people": [ - { + "investigation": { + "identifier": "", + "title": "Bob's investigation", + "description": "", + "submissionDate": "", + "publicReleaseDate": "", + "ontologySourceReferences": [], + "filename": "i_Bob's investigation.txt", + "comments": [ + { + "name": "ISAjson export time", + "value": "2022-11-07T08:09:59Z" + }, + { + "name": "SEEK Project name", + "value": "Bob's PhD project" + }, + { + "name": "SEEK Project ID", + "value": "http://localhost:3000/single_pages/2" + }, + { + "name": "SEEK Investigation ID", + "value": "19" + } + ], + "publications": [], + "people": [ + { "lastName": "Bob", "firstName": "Bob", "midInitials": "", @@ -83,940 +37,1091 @@ "address": "", "affiliation": "", "roles": [ - { - "termAccession": "", - "termSource": "", - "annotationValue": "" - } + { + "termAccession": "", + "termSource": "", + "annotationValue": "" + } ], "comments": [ - { - "name": "", - "value": "", - "@id": "" - } + { + "name": "", + "value": "", + "@id": "" + } ], "@id": "#people/5" - } - ], - "studyDesignDescriptors": [], - "characteristicCategories": [ - { - "characteristicType": { - "annotationValue": "Title", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/Title_317" - }, - { - "characteristicType": { - "annotationValue": "Description", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/Description_318" - }, - { - "characteristicType": { - "annotationValue": "tax_id", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/tax_id_319" - }, - { - "characteristicType": { - "annotationValue": "organism", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/organism_320" - }, - { - "characteristicType": { - "annotationValue": "cell_type", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/cell_type_321" - }, - { - "characteristicType": { - "annotationValue": "dev_stage", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/dev_stage_322" - }, - { - "characteristicType": { - "annotationValue": "collection_date", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/collection_date_323" - }, - { - "characteristicType": { - "annotationValue": "isolation_source", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/isolation_source_324" - }, - { - "characteristicType": { - "annotationValue": "collected_by", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/collected_by_325" - }, - { - "characteristicType": { - "annotationValue": "geographic location (country and/or sea)", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" - }, - { - "characteristicType": { - "annotationValue": "submission date", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/submission_date_327" - }, - { - "characteristicType": { - "annotationValue": "status", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/status_328" - }, - { - "characteristicType": { - "annotationValue": "accession", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/accession_329" - } - ], - "materials": { - "sources": [ - { - "name": "plant 1", - "characteristics": [ - { - "category": { - "@id": "#characteristic_category/Title_317" - }, - "value": { - "annotationValue": "plant 1", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/Description_318" - }, - "value": { - "annotationValue": "plant in the lab", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/tax_id_319" - }, - "value": { - "annotationValue": "NCBI:txid3702", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/organism_320" - }, - "value": { - "annotationValue": "Arabidopsis thaliana", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/cell_type_321" - }, - "value": { - "annotationValue": "na", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/dev_stage_322" - }, - "value": { - "annotationValue": "budding", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/collection_date_323" - }, - "value": { - "annotationValue": "01/01/2022", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/isolation_source_324" + } + ], + "studies": [ + { + "identifier": "", + "title": "Integrated multi-omics profiling of Arabidopsis thaliana under controlled experimental conditions", + "description": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions.\r\n", + "submissionDate": "", + "publicReleaseDate": "", + "filename": "s_Arabidopsis thaliana.txt", + "comments": [ + { + "name": "SEEK Study ID", + "value": "10" + }, + { + "name": "SEEK creation date", + "value": "2022-11-03T16:20:49Z" + } + ], + "publications": [], + "people": [ + { + "lastName": "Bob", + "firstName": "Bob", + "midInitials": "", + "email": "bob@testing.com", + "phone": "", + "fax": "", + "address": "", + "affiliation": "", + "roles": [ + { + "termAccession": "", + "termSource": "", + "annotationValue": "" + } + ], + "comments": [ + { + "name": "", + "value": "", + "@id": "" + } + ], + "@id": "#people/5" + } + ], + "studyDesignDescriptors": [], + "characteristicCategories": [ + { + "characteristicType": { + "annotationValue": "Title", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "seed", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/Title_317" + }, + { + "characteristicType": { + "annotationValue": "Description", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/collected_by_325" + "@id": "#characteristic_category/Description_318" + }, + { + "characteristicType": { + "annotationValue": "tax_id", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "Bob", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/tax_id_319" + }, + { + "characteristicType": { + "annotationValue": "organism", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" + "@id": "#characteristic_category/organism_320" + }, + { + "characteristicType": { + "annotationValue": "dev_stage", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "Belgium", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/dev_stage_322" + }, + { + "characteristicType": { + "annotationValue": "collection_date", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/submission_date_327" + "@id": "#characteristic_category/collection_date_323" + }, + { + "characteristicType": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/isolation_source_324" + }, + { + "characteristicType": { + "annotationValue": "collected_by", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/status_328" + "@id": "#characteristic_category/collected_by_325" + }, + { + "characteristicType": { + "annotationValue": "geographic location (country and/or sea)", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" + }, + { + "characteristicType": { + "annotationValue": "submission date", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/accession_329" + "@id": "#characteristic_category/submission_date_327" + }, + { + "characteristicType": { + "annotationValue": "status", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/status_328" + }, + { + "characteristicType": { + "annotationValue": "accession", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/accession" + "@id": "#characteristic_category/accession_329" + }, + { + "characteristicType": { + "annotationValue": "genotype", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "SAMEA130788488" - } - } - ], - "@id": "#source/330" - } - ], - "samples": [ - { - "name": "leaf 1", - "derivesFrom": [ - { - "@id": "#source/330" - } - ], - "characteristics": [ - { - "category": { - "@id": "#characteristic_category/accession" + "@id": "#characteristic_category/genotype_330" + }, + { + "characteristicType": { + "annotationValue": "growth condition", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "SAMEA130788489" - } - } - ], - "factorValues": [ - { - "category": { - "@id": "" + "@id": "#characteristic_category/growth_condition_331" + }, + { + "characteristicType": { + "annotationValue": "organism part", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/organism_part_332" + }, + { + "characteristicType": { + "annotationValue": "sample description", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - } - ], - "@id": "#sample/331" - } - ] - }, - "protocols": [ - { - "name": "sample collection", - "protocolType": { - "annotationValue": "sample collection", - "termAccession": "", - "termSource": "" - }, - "description": "", - "uri": "", - "version": "", - "parameters": [], - "components": [ - { - "componentName": "", - "componentType": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - } - ], - "@id": "#protocol/18_10" - }, - { - "name": "nucleic acid extraction", - "protocolType": { - "annotationValue": "nucleic acid extraction", - "termAccession": "", - "termSource": "" - }, - "description": "", - "uri": "", - "version": "", - "parameters": [], - "components": [ - { - "componentName": "", - "componentType": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - } - ], - "@id": "#protocol/19_18" - }, - { - "name": "library construction", - "protocolType": { - "annotationValue": "library construction", - "termAccession": "", - "termSource": "" - }, - "description": "", - "uri": "", - "version": "", - "parameters": [ - { - "parameterName": { - "annotationValue": "library_construction_protocol", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/349" - }, - { - "parameterName": { - "annotationValue": "design_description", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/351" - }, - { - "parameterName": { - "annotationValue": "library source", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/352" - }, - { - "parameterName": { - "annotationValue": "library strategy", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/353" - }, - { - "parameterName": { - "annotationValue": "library selection", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/354" - }, - { - "parameterName": { - "annotationValue": "library layout", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/355" - }, - { - "parameterName": { - "annotationValue": "insert size", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/356" - } - ], - "components": [ - { - "componentName": "", - "componentType": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - } - ], - "@id": "#protocol/20_20" - }, - { - "name": "nucleic acid sequencing", - "protocolType": { - "annotationValue": "nucleic acid sequencing", - "termAccession": "", - "termSource": "" - }, - "description": "", - "uri": "", - "version": "", - "parameters": [ - { - "parameterName": { - "annotationValue": "sequencing instrument", - "termAccession": "", - "termSource": "" - }, - "@id": "#parameter/363" - } - ], - "components": [ - { - "componentName": "", - "componentType": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - } - } - ], - "@id": "#protocol/21_21" - } - ], - "processSequence": [ - { - "name": "", - "executesProtocol": { - "@id": "#protocol/18_10" - }, - "parameterValues": [], - "performer": "", - "date": "", - "previousProcess": {}, - "nextProcess": {}, - "inputs": [ - { - "@id": "#source/330" - } - ], - "outputs": [ - { - "@id": "#sample/331" - } - ], - "@id": "#process/sample_collection/331" - } - ], - "assays": [ - { - "filename": "a_assays.txt", - "measurementType": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "technologyType": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "technologyPlatform": "", - "characteristicCategories": [ - { - "characteristicType": { - "annotationValue": "Title", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/Title_350" - }, - { - "characteristicType": { - "annotationValue": "submission date", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/submission_date_358" - }, - { - "characteristicType": { - "annotationValue": "status", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/status_359" - }, - { - "characteristicType": { - "annotationValue": "accession", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/accession_360" - } + "@id": "#characteristic_category/sample_description_333" + } ], "materials": { - "samples": [ - { - "@id": "#sample/331" - } - ], - "otherMaterials": [ - { - "name": "extract 1", - "type": "Extract Name", - "characteristics": [], - "derivesFrom": [ - { - "@id": "#sample/331" - } - ], - "@id": "#other_material/332" - }, - { - "name": "library 1", - "type": "Extract Name", - "characteristics": [ - { - "category": { - "@id": "#characteristic_category/Title_350" - }, - "value": { - "annotationValue": "library 1", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/submission_date_358" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/status_359" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - } - ], - "derivesFrom": [ - { - "@id": "#other_material/332" - } - ], - "@id": "#other_material/333" - } - ] - }, - "processSequence": [ - { - "name": "", - "executesProtocol": { - "@id": "#protocol/19_18" - }, - "parameterValues": [], - "performer": "", - "date": "", - "previousProcess": { - "@id": "#process/sample_collection/332" - }, - "nextProcess": { - "@id": "#process/library_construction/332" - }, - "inputs": [ + "sources": [ { - "@id": "#sample/331" + "name": "plant 1", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_317" + }, + "value": { + "annotationValue": "plant 1", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/Description_318" + }, + "value": { + "annotationValue": "plant in the lab", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/tax_id_319" + }, + "value": { + "annotationValue": "NCBI:txid3702", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/organism_320" + }, + "value": { + "annotationValue": "Arabidopsis thaliana", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/genotype_330" + }, + "value": { + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" + }, + "value": { + "annotationValue": "16 h light / 8 h dark growth chamber", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/dev_stage_322" + }, + "value": { + "annotationValue": "vegetative rosette stage", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/collection_date_323" + }, + "value": { + "annotationValue": "2022-01-01", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/isolation_source_324" + }, + "value": { + "annotationValue": "whole plant", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/collected_by_325" + }, + "value": { + "annotationValue": "Bob", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" + }, + "value": { + "annotationValue": "Belgium", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/submission_date_327" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/status_328" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/accession_329" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/accession" + }, + "value": { + "annotationValue": "SAMEA130788488" + } + } + ], + "@id": "#source/330" } - ], - "outputs": [ + ], + "samples": [ { - "@id": "#other_material/332" + "name": "leaf 1", + "derivesFrom": [ + { + "@id": "#source/330" + } + ], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/organism_part_332" + }, + "value": { + "annotationValue": "leaf", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/sample_description_333" + }, + "value": { + "annotationValue": "young rosette leaf collected for DNA extraction", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/accession" + }, + "value": { + "annotationValue": "SAMEA130788489" + } + } + ], + "factorValues": [ + { + "category": { + "@id": "" + }, + "value": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + } + ], + "@id": "#sample/331" } - ], - "@id": "#process/nucleic_acid_extraction/332" - }, - { - "name": "", - "executesProtocol": { - "@id": "#protocol/20_20" - }, - "parameterValues": [ - { - "category": { - "@id": "#parameter/349" - }, - "value": { - "annotationValue": "lib prep", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#parameter/351" - }, - "value": { - "annotationValue": "Test", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#parameter/352" - }, - "value": { - "annotationValue": "OTHER", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } + ] + }, + "protocols": [ + { + "name": "sample collection", + "protocolType": { + "annotationValue": "sample collection", + "termAccession": "", + "termSource": "" }, - { - "category": { - "@id": "#parameter/353" - }, - "value": { - "annotationValue": "OTHER", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } + "description": "", + "uri": "", + "version": "", + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_method", + "parameterName": { + "annotationValue": "collection method", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_preservation", + "parameterName": { + "annotationValue": "sample preservation", + "termAccession": "", + "termSource": "" + } + } + ], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ], + "@id": "#protocol/18_10" + }, + { + "name": "nucleic acid extraction", + "protocolType": { + "annotationValue": "nucleic acid extraction", + "termAccession": "", + "termSource": "" }, - { - "category": { - "@id": "#parameter/354" - }, - "value": { - "annotationValue": "RT-PCR", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } + "description": "", + "uri": "", + "version": "", + "parameters": [], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ], + "@id": "#protocol/19_18" + }, + { + "name": "library construction", + "protocolType": { + "annotationValue": "library construction", + "termAccession": "", + "termSource": "" }, - { - "category": { - "@id": "#parameter/355" - }, - "value": { - "annotationValue": "SINGLE", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } + "description": "", + "uri": "", + "version": "", + "parameters": [ + { + "parameterName": { + "annotationValue": "LIBRARY_CONSTRUCTION_PROTOCOL", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/349" + }, + { + "parameterName": { + "annotationValue": "LIBRARY_NAME", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/350" + }, + { + "parameterName": { + "annotationValue": "DESIGN_DESCRIPTION", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/351" + }, + { + "parameterName": { + "annotationValue": "LIBRARY_SOURCE", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/352" + }, + { + "parameterName": { + "annotationValue": "LIBRARY_STRATEGY", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/353" + }, + { + "parameterName": { + "annotationValue": "LIBRARY_SELECTION", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/354" + }, + { + "parameterName": { + "annotationValue": "LIBRARY_LAYOUT", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/355" + }, + { + "parameterName": { + "annotationValue": "NOMINAL_LENGTH", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/356" + } + ], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ], + "@id": "#protocol/20_20" + }, + { + "name": "nucleic acid sequencing", + "protocolType": { + "annotationValue": "nucleic acid sequencing", + "termAccession": "", + "termSource": "" }, - { - "category": { - "@id": "#parameter/356" - }, - "value": { - "annotationValue": "100", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - } - ], - "performer": "", - "date": "", - "previousProcess": { - "@id": "#process/nucleic_acid_extraction/333" - }, - "nextProcess": { - "@id": "#process/nucleic_acid_sequencing/333" - }, - "inputs": [ - { - "@id": "#other_material/332" - } - ], - "outputs": [ - { - "@id": "#other_material/333" - } - ], - "@id": "#process/library_construction/333" - }, - { - "name": "", - "executesProtocol": { + "description": "", + "uri": "", + "version": "", + "parameters": [ + { + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/362" + }, + { + "parameterName": { + "annotationValue": "INSTRUMENT_MODEL", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/363" + } + ], + "components": [ + { + "componentName": "", + "componentType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" + } + } + ], "@id": "#protocol/21_21" - }, - "parameterValues": [ - { - "category": { - "@id": "#parameter/363" - }, - "value": { - "annotationValue": " MinION", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - } - ], - "performer": "", - "date": "", - "previousProcess": { - "@id": "#process/library_construction/334" - }, - "nextProcess": {}, - "inputs": [ - { - "@id": "#other_material/333" - } - ], - "outputs": [ - { - "@id": "#data_file/334" - } - ], - "@id": "#process/nucleic_acid_sequencing/334" - } + } ], - "dataFiles": [ - { - "name": "fake2.bam", - "type": "Raw Data File", - "comments": [ - { - "name": "file type", - "value": "bam" + "processSequence": [ + { + "name": "", + "executesProtocol": { + "@id": "#protocol/18_10" }, - { - "name": "file checksum", - "value": "9840f585055afc37de353706fd31a377" + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "leaf tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_method" + }, + "value": { + "annotationValue": "sterile scalpel excision", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_preservation" + }, + "value": { + "annotationValue": "flash frozen in liquid nitrogen", + "termSource": "", + "termAccession": "" + } + } + ], + "performer": "", + "date": "", + "previousProcess": {}, + "nextProcess": {}, + "inputs": [ + { + "@id": "#source/330" + } + ], + "outputs": [ + { + "@id": "#sample/331" + } + ], + "@id": "#process/sample_collection/331" + } + ], + "assays": [ + { + "comments": [ + { + "name": "target_repository", + "value": "ena" + }, + { + "name": "STUDY_ABSTRACT", + "value": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions." + }, + { + "name": "STUDY_TYPE", + "value": "Other" + }, + { + "name": "new_study_type", + "value": "Amplicon sequencing" + } + ], + "filename": "a_assays.txt", + "measurementType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" }, - { - "name": "submission date", - "value": "" + "technologyType": { + "annotationValue": "", + "termSource": "", + "termAccession": "" }, - { - "name": "status", - "value": "" + "technologyPlatform": "", + "characteristicCategories": [ + { + "characteristicType": { + "annotationValue": "Title", + "termAccession": "", + "termSource": "" + }, + "@id": "#characteristic_category/Title_350" + } + ], + "materials": { + "samples": [ + { + "@id": "#sample/331" + } + ], + "otherMaterials": [ + { + "name": "extract 1", + "type": "Extract Name", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_350" + }, + "value": { + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", + "termAccession": "", + "termSource": "" + } + } + ], + "derivesFrom": [ + { + "@id": "#sample/331" + } + ], + "@id": "#other_material/332" + }, + { + "name": "library 1", + "type": "Extract Name", + "characteristics": [], + "derivesFrom": [ + { + "@id": "#other_material/332" + } + ], + "@id": "#other_material/333" + } + ] }, - { - "name": "accession", - "value": "" - } - ], - "@id": "#data/334" - } + "processSequence": [ + { + "name": "", + "executesProtocol": { + "@id": "#protocol/19_18" + }, + "parameterValues": [], + "performer": "", + "date": "", + "previousProcess": { + "@id": "#process/sample_collection/332" + }, + "nextProcess": { + "@id": "#process/library_construction/332" + }, + "inputs": [ + { + "@id": "#sample/331" + } + ], + "outputs": [ + { + "@id": "#other_material/332" + } + ], + "@id": "#process/nucleic_acid_extraction/332" + }, + { + "name": "", + "executesProtocol": { + "@id": "#protocol/20_20" + }, + "parameterValues": [ + { + "category": { + "@id": "#parameter/349" + }, + "value": { + "annotationValue": "PCR amplicon library preparation for Arabidopsis leaf DNA", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/350" + }, + "value": { + "annotationValue": "arabidopsis_leaf_amplicon_library", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/351" + }, + "value": { + "annotationValue": "Amplicon sequencing of Arabidopsis thaliana leaf DNA.", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/352" + }, + "value": { + "annotationValue": "GENOMIC", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/353" + }, + "value": { + "annotationValue": "AMPLICON", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/354" + }, + "value": { + "annotationValue": "PCR", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/355" + }, + "value": { + "annotationValue": "SINGLE", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/356" + }, + "value": { + "annotationValue": "100", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + } + ], + "performer": "", + "date": "", + "previousProcess": { + "@id": "#process/nucleic_acid_extraction/333" + }, + "nextProcess": { + "@id": "#process/nucleic_acid_sequencing/333" + }, + "inputs": [ + { + "@id": "#other_material/332" + } + ], + "outputs": [ + { + "@id": "#other_material/333" + } + ], + "@id": "#process/library_construction/333" + }, + { + "name": "", + "executesProtocol": { + "@id": "#protocol/21_21" + }, + "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "OXFORD_NANOPORE", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/363" + }, + "value": { + "annotationValue": "MinION", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + } + ], + "performer": "", + "date": "", + "previousProcess": { + "@id": "#process/library_construction/334" + }, + "nextProcess": {}, + "inputs": [ + { + "@id": "#other_material/333" + } + ], + "outputs": [ + { + "@id": "#data_file/334" + } + ], + "@id": "#process/nucleic_acid_sequencing/334" + } + ], + "dataFiles": [ + { + "name": "fake2.bam", + "type": "Raw Data File", + "comments": [ + { + "name": "file type", + "value": "bam" + }, + { + "name": "file checksum", + "value": "9840f585055afc37de353706fd31a377" + }, + { + "name": "submission date", + "value": "" + }, + { + "name": "status", + "value": "" + }, + { + "name": "accession", + "value": "" + } + ], + "@id": "#data/334" + } + ], + "unitCategories": [], + "@id": "#assay/18_20_21" + } ], - "unitCategories": [], - "@id": "#assay/18_20_21" - } - ], - "factors": [], - "unitCategories": [] - } - ] - } + "factors": [], + "unitCategories": [] + } + ] + } } diff --git a/test-data/biosamples-original-isa-no-accesion-char.json b/test-data/biosamples-original-isa-no-accesion-char.json index 77e786ff..6c1747df 100644 --- a/test-data/biosamples-original-isa-no-accesion-char.json +++ b/test-data/biosamples-original-isa-no-accesion-char.json @@ -56,8 +56,8 @@ "studies": [ { "identifier": "", - "title": "Arabidopsis thaliana", - "description": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis.\r\n", + "title": "Integrated multi-omics profiling of Arabidopsis thaliana under controlled experimental conditions", + "description": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions.\r\n", "submissionDate": "", "publicReleaseDate": "", "filename": "s_Arabidopsis thaliana.txt", @@ -133,14 +133,6 @@ "termSource": "" } }, - { - "@id": "#characteristic_category/cell_type_321", - "characteristicType": { - "annotationValue": "cell_type", - "termAccession": "", - "termSource": "" - } - }, { "@id": "#characteristic_category/dev_stage_322", "characteristicType": { @@ -196,6 +188,38 @@ "termAccession": "", "termSource": "" } + }, + { + "@id": "#characteristic_category/genotype_330", + "characteristicType": { + "annotationValue": "genotype", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/growth_condition_331", + "characteristicType": { + "annotationValue": "growth condition", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/organism_part_332", + "characteristicType": { + "annotationValue": "organism part", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/sample_description_333", + "characteristicType": { + "annotationValue": "sample description", + "termAccession": "", + "termSource": "" + } } ], "materials": { @@ -266,10 +290,25 @@ }, { "category": { - "@id": "#characteristic_category/cell_type_321" + "@id": "#characteristic_category/genotype_330" }, "value": { - "annotationValue": "na", + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" + }, + "value": { + "annotationValue": "16 h light / 8 h dark growth chamber", "termSource": "", "termAccession": "" }, @@ -284,7 +323,7 @@ "@id": "#characteristic_category/dev_stage_322" }, "value": { - "annotationValue": "budding", + "annotationValue": "vegetative rosette stage", "termSource": "", "termAccession": "" }, @@ -299,7 +338,7 @@ "@id": "#characteristic_category/collection_date_323" }, "value": { - "annotationValue": "01/01/2022", + "annotationValue": "2022-01-01", "termSource": "", "termAccession": "" }, @@ -314,7 +353,7 @@ "@id": "#characteristic_category/isolation_source_324" }, "value": { - "annotationValue": "seed", + "annotationValue": "whole plant", "termSource": "", "termAccession": "" }, @@ -396,7 +435,28 @@ "@id": "#source/330" } ], - "characteristics": [], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/organism_part_332" + }, + "value": { + "annotationValue": "leaf", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/sample_description_333" + }, + "value": { + "annotationValue": "young rosette leaf collected for DNA extraction", + "termSource": "", + "termAccession": "" + } + } + ], "factorValues": [ { "category": { @@ -429,7 +489,32 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_method", + "parameterName": { + "annotationValue": "collection method", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_preservation", + "parameterName": { + "annotationValue": "sample preservation", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -584,7 +669,38 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "leaf tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_method" + }, + "value": { + "annotationValue": "sterile scalpel excision", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_preservation" + }, + "value": { + "annotationValue": "flash frozen in liquid nitrogen", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, @@ -624,36 +740,12 @@ "termAccession": "", "termSource": "" } - }, - { - "@id": "#characteristic_category/submission_date_358", - "characteristicType": { - "annotationValue": "submission date", - "termAccession": "", - "termSource": "" - } - }, - { - "@id": "#characteristic_category/status_359", - "characteristicType": { - "annotationValue": "status", - "termAccession": "", - "termSource": "" - } - }, - { - "@id": "#characteristic_category/accession_360", - "characteristicType": { - "annotationValue": "accession", - "termAccession": "", - "termSource": "" - } } ], "comments": [ { - "name": "target_repository", - "value": "ena" + "name": "target_repository", + "value": "ena" } ], "materials": { @@ -667,79 +759,29 @@ "@id": "#other_material/332", "name": "extract 1", "type": "Extract Name", - "characteristics": [], - "derivesFrom": [ - { - "@id": "#sample/331" - } - ] - }, - { - "@id": "#other_material/333", - "name": "library 1", - "type": "Extract Name", "characteristics": [ { "category": { "@id": "#characteristic_category/Title_350" }, "value": { - "annotationValue": "library 1", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/submission_date_358" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/status_359" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] + "termSource": "" } } ], + "derivesFrom": [ + { + "@id": "#sample/331" + } + ] + }, + { + "@id": "#other_material/333", + "name": "library 1", + "type": "Extract Name", + "characteristics": [], "derivesFrom": [ { "@id": "#other_material/332" diff --git a/test-data/biosamples-original-isa.json b/test-data/biosamples-original-isa.json index 0554c189..930459a6 100644 --- a/test-data/biosamples-original-isa.json +++ b/test-data/biosamples-original-isa.json @@ -56,8 +56,8 @@ "studies": [ { "identifier": "", - "title": "Arabidopsis thaliana", - "description": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis.\r\n", + "title": "Integrated multi-omics profiling of Arabidopsis thaliana under controlled experimental conditions", + "description": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions.\r\n", "submissionDate": "", "publicReleaseDate": "", "filename": "s_Arabidopsis thaliana.txt", @@ -133,14 +133,6 @@ "termSource": "" } }, - { - "@id": "#characteristic_category/cell_type_321", - "characteristicType": { - "annotationValue": "cell_type", - "termAccession": "", - "termSource": "" - } - }, { "@id": "#characteristic_category/dev_stage_322", "characteristicType": { @@ -204,6 +196,38 @@ "termAccession": "", "termSource": "" } + }, + { + "@id": "#characteristic_category/genotype_330", + "characteristicType": { + "annotationValue": "genotype", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/growth_condition_331", + "characteristicType": { + "annotationValue": "growth condition", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/organism_part_332", + "characteristicType": { + "annotationValue": "organism part", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#characteristic_category/sample_description_333", + "characteristicType": { + "annotationValue": "sample description", + "termAccession": "", + "termSource": "" + } } ], "materials": { @@ -274,10 +298,25 @@ }, { "category": { - "@id": "#characteristic_category/cell_type_321" + "@id": "#characteristic_category/genotype_330" }, "value": { - "annotationValue": "na", + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" + }, + "value": { + "annotationValue": "16 h light / 8 h dark growth chamber", "termSource": "", "termAccession": "" }, @@ -292,7 +331,7 @@ "@id": "#characteristic_category/dev_stage_322" }, "value": { - "annotationValue": "budding", + "annotationValue": "vegetative rosette stage", "termSource": "", "termAccession": "" }, @@ -307,7 +346,7 @@ "@id": "#characteristic_category/collection_date_323" }, "value": { - "annotationValue": "01/01/2022", + "annotationValue": "2022-01-01", "termSource": "", "termAccession": "" }, @@ -322,7 +361,7 @@ "@id": "#characteristic_category/isolation_source_324" }, "value": { - "annotationValue": "seed", + "annotationValue": "whole plant", "termSource": "", "termAccession": "" }, @@ -419,7 +458,28 @@ "@id": "#source/330" } ], - "characteristics": [], + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/organism_part_332" + }, + "value": { + "annotationValue": "leaf", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/sample_description_333" + }, + "value": { + "annotationValue": "young rosette leaf collected for DNA extraction", + "termSource": "", + "termAccession": "" + } + } + ], "factorValues": [ { "category": { @@ -452,7 +512,32 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_method", + "parameterName": { + "annotationValue": "collection method", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/sample_collection_preservation", + "parameterName": { + "annotationValue": "sample preservation", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -502,7 +587,15 @@ { "@id": "#parameter/349", "parameterName": { - "annotationValue": "library_construction_protocol", + "annotationValue": "LIBRARY_CONSTRUCTION_PROTOCOL", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/350", + "parameterName": { + "annotationValue": "LIBRARY_NAME", "termAccession": "", "termSource": "" } @@ -510,7 +603,7 @@ { "@id": "#parameter/351", "parameterName": { - "annotationValue": "design_description", + "annotationValue": "DESIGN_DESCRIPTION", "termAccession": "", "termSource": "" } @@ -518,7 +611,7 @@ { "@id": "#parameter/352", "parameterName": { - "annotationValue": "library source", + "annotationValue": "LIBRARY_SOURCE", "termAccession": "", "termSource": "" } @@ -526,7 +619,7 @@ { "@id": "#parameter/353", "parameterName": { - "annotationValue": "library strategy", + "annotationValue": "LIBRARY_STRATEGY", "termAccession": "", "termSource": "" } @@ -534,7 +627,7 @@ { "@id": "#parameter/354", "parameterName": { - "annotationValue": "library selection", + "annotationValue": "LIBRARY_SELECTION", "termAccession": "", "termSource": "" } @@ -542,7 +635,7 @@ { "@id": "#parameter/355", "parameterName": { - "annotationValue": "library layout", + "annotationValue": "LIBRARY_LAYOUT", "termAccession": "", "termSource": "" } @@ -550,7 +643,7 @@ { "@id": "#parameter/356", "parameterName": { - "annotationValue": "insert size", + "annotationValue": "NOMINAL_LENGTH", "termAccession": "", "termSource": "" } @@ -579,10 +672,18 @@ "uri": "", "version": "", "parameters": [ + { + "@id": "#parameter/362", + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + } + }, { "@id": "#parameter/363", "parameterName": { - "annotationValue": "sequencing instrument", + "annotationValue": "INSTRUMENT_MODEL", "termAccession": "", "termSource": "" } @@ -607,7 +708,38 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "leaf tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_method" + }, + "value": { + "annotationValue": "sterile scalpel excision", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#parameter/sample_collection_preservation" + }, + "value": { + "annotationValue": "flash frozen in liquid nitrogen", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, @@ -647,30 +779,24 @@ "termAccession": "", "termSource": "" } + } + ], + "comments": [ + { + "name": "target_repository", + "value": "ena" }, { - "@id": "#characteristic_category/submission_date_358", - "characteristicType": { - "annotationValue": "submission date", - "termAccession": "", - "termSource": "" - } + "name": "STUDY_ABSTRACT", + "value": "A coordinated multi-omics study combining sequencing-based and molecular profiling approaches to characterize Arabidopsis thaliana responses under controlled experimental conditions." }, { - "@id": "#characteristic_category/status_359", - "characteristicType": { - "annotationValue": "status", - "termAccession": "", - "termSource": "" - } + "name": "STUDY_TYPE", + "value": "Other" }, { - "@id": "#characteristic_category/accession_360", - "characteristicType": { - "annotationValue": "accession", - "termAccession": "", - "termSource": "" - } + "name": "new_study_type", + "value": "Amplicon sequencing" } ], "materials": { @@ -684,79 +810,29 @@ "@id": "#other_material/332", "name": "extract 1", "type": "Extract Name", - "characteristics": [], - "derivesFrom": [ - { - "@id": "#sample/331" - } - ] - }, - { - "@id": "#other_material/333", - "name": "library 1", - "type": "Extract Name", "characteristics": [ { "category": { "@id": "#characteristic_category/Title_350" }, "value": { - "annotationValue": "library 1", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/submission_date_358" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/status_359" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] + "termSource": "" } } ], + "derivesFrom": [ + { + "@id": "#sample/331" + } + ] + }, + { + "@id": "#other_material/333", + "name": "library 1", + "type": "Extract Name", + "characteristics": [], "derivesFrom": [ { "@id": "#other_material/332" @@ -804,7 +880,22 @@ "@id": "#parameter/349" }, "value": { - "annotationValue": "lib prep", + "annotationValue": "PCR amplicon library preparation for Arabidopsis leaf DNA", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/350" + }, + "value": { + "annotationValue": "arabidopsis_leaf_amplicon_library", "termSource": "", "termAccession": "" }, @@ -819,7 +910,7 @@ "@id": "#parameter/351" }, "value": { - "annotationValue": "Test", + "annotationValue": "Amplicon sequencing of Arabidopsis thaliana leaf DNA.", "termSource": "", "termAccession": "" }, @@ -834,7 +925,7 @@ "@id": "#parameter/352" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "GENOMIC", "termSource": "", "termAccession": "" }, @@ -849,7 +940,7 @@ "@id": "#parameter/353" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "AMPLICON", "termSource": "", "termAccession": "" }, @@ -864,7 +955,7 @@ "@id": "#parameter/354" }, "value": { - "annotationValue": "RT-PCR", + "annotationValue": "PCR", "termSource": "", "termAccession": "" }, @@ -931,12 +1022,27 @@ "@id": "#protocol/21_21" }, "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "OXFORD_NANOPORE", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, { "category": { "@id": "#parameter/363" }, "value": { - "annotationValue": " MinION", + "annotationValue": "MinION", "termSource": "", "termAccession": "" }, diff --git a/test-data/ena-receipt-invalid.json b/test-data/ena-receipt-invalid.json index c882c0eb..5402b0d2 100644 --- a/test-data/ena-receipt-invalid.json +++ b/test-data/ena-receipt-invalid.json @@ -22,7 +22,7 @@ ], "studies":[ { - "alias":"Arabidopsis thaliana-0.5578006304577448", + "alias":"#assay/18_20_21-0.5578006304577448", "status":"PRIVATE", "holdUntilDate": "2023-01-01Z", "externalAccession": { @@ -33,7 +33,7 @@ ], "projects": [ { - "alias": "Bob's investigation-0.5578006304577448", + "alias": "#assay/18_20_21-0.5578006304577448", "accession": "PRJEB82365", "status": "PRIVATE", "holdUntilDate": "2023-01-01Z", @@ -55,4 +55,4 @@ "actions": [ "ADD" ] -} \ No newline at end of file +} diff --git a/test-data/ena-receipt.json b/test-data/ena-receipt.json index ab3bb532..aa0ff205 100644 --- a/test-data/ena-receipt.json +++ b/test-data/ena-receipt.json @@ -29,7 +29,7 @@ ], "projects": [ { - "alias": "Arabidopsis thaliana", + "alias": "#assay/18_20_21", "accession": "PRJEB101337", "status": "PRIVATE", "holdUntilDate": "2023-01-01Z", @@ -52,4 +52,4 @@ "ADD", "HOLD" ] -} \ No newline at end of file +} diff --git a/test-data/mars-ena-receipt-invalid.json b/test-data/mars-ena-receipt-invalid.json index 475a5720..2c863af0 100644 --- a/test-data/mars-ena-receipt-invalid.json +++ b/test-data/mars-ena-receipt-invalid.json @@ -3,7 +3,7 @@ "errors": [ { "type": "INVALID_METADATA", - "message": "ENA receipt: Accession number of Arabidopsis thaliana-0.5578006304577448 is NULL" + "message": "ENA receipt: Accession number of #assay/18_20_21-0.5578006304577448 is NULL" } ], "info": [ @@ -77,4 +77,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/test-data/mars-ena-receipt.json b/test-data/mars-ena-receipt.json index 727bde42..ff40fd6d 100644 --- a/test-data/mars-ena-receipt.json +++ b/test-data/mars-ena-receipt.json @@ -19,6 +19,13 @@ "key": "title", "value": "Arabidopsis thaliana" } + }, + { + "key": "assays", + "where": { + "key": "@id", + "value": "#assay/18_20_21" + } } ] }, @@ -84,4 +91,4 @@ ] } ] -} \ No newline at end of file +}