From 8e8e5fac2d9a9e2a35a4d7c4f21f6bfc648d0ecc Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 18:55:48 +0200 Subject: [PATCH 01/27] Improve study and experiment handling --- .../sra/service/MarsReceiptService.java | 8 +- .../service/WebinExperimentXmlCreator.java | 445 +++++++++++------- .../sra/service/WebinProjectXmlCreator.java | 108 +++-- .../sra/service/WebinStudyXmlCreator.java | 204 +++++--- .../WebinExperimentXmlCreatorTest.java | 28 ++ .../ISAToSRA/receipt/MarsReceiptProvider.java | 64 ++- test-data/biosamples-input-isa.json | 208 +++++--- test-data/biosamples-modified-isa.json | 102 +++- test-data/biosamples-original-isa.json | 114 ++++- test-data/ena-receipt-invalid.json | 6 +- test-data/ena-receipt.json | 4 +- test-data/mars-ena-receipt-invalid.json | 4 +- test-data/mars-ena-receipt.json | 9 +- 13 files changed, 930 insertions(+), 374 deletions(-) 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..ffd0765e 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,10 @@ 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.marsmodel.MarsError; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsErrorType; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsReceipt; @@ -82,7 +82,9 @@ 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, @@ -131,7 +133,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/WebinExperimentXmlCreator.java b/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinExperimentXmlCreator.java index b0949430..65cea49c 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,7 +2,15 @@ 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.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.Parameter; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ParameterValue; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ProcessSequence; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,6 +23,13 @@ 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 +37,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,122 +52,172 @@ 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()); - })); - - return protocolToParameterMap; + 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 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 void mapExperimentForDataFile( + final Element root, + final Map> protocolToParameterNameMap, + final Map bioSampleAccessions, + final String randomSubmissionIdentifier, + final Map experimentSequence, + final Study study, + final com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, + final DataFile dataFile) { + final ProcessSequence sequencingProcess = + findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); + + if (sequencingProcess == null) { + return; + } + + final OtherMaterial library = + findLibraryFromProcessInput(sequencingProcess, assay.getMaterials()); + + if (library == null || experimentSequence.containsKey(library.getId())) { + return; + } + + final String experimentId = library.getId() + "-" + randomSubmissionIdentifier; + experimentSequence.put(library.getId(), experimentId); + + final ProcessSequence libraryConstructionProcess = + findProcessByOutputId(assay.getProcessSequence(), library.getId()); + + createExperimentElement( + root, + library, + assay, + libraryConstructionProcess, + sequencingProcess, + protocolToParameterNameMap, + bioSampleAccessions, + experimentId, + randomSubmissionIdentifier); + } + 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; - } - } + if (process.getOutputs() == null) { + continue; + } + + for (final Output output : process.getOutputs()) { + if (output.getId() == null) { + continue; + } + + 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) { @@ -160,136 +225,186 @@ private OtherMaterial findLibraryFromProcessInput( } 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; - } + if (input.getId() == null) { + continue; + } + + for (final OtherMaterial otherMaterial : materials.getOtherMaterials()) { + if (otherMaterial.getId() != null && otherMaterial.getId().equals(input.getId())) { + return otherMaterial; } } } + 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 com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, final ProcessSequence libraryConstructionProcess, - final Map> protocolToParameterMap, + final ProcessSequence sequencingProcess, + final Map> protocolToParameterNameMap, final Map bioSampleAccessions, final String experimentId, final String randomSubmissionIdentifier) { - final Element experimentElement = root.addElement("EXPERIMENT"); + final Map libraryParameters = + extractParameterValues(libraryConstructionProcess, protocolToParameterNameMap); + final Map sequencingParameters = + extractParameterValues(sequencingProcess, protocolToParameterNameMap); + final Element experimentElement = root.addElement("EXPERIMENT"); experimentElement.addAttribute("alias", experimentId); - experimentElement.addElement("TITLE").addText(library.getName()); + experimentElement + .addElement("TITLE") + .addText(firstNonBlank(libraryParameters.get("TITLE"), libraryParameters.get("LIBRARY_NAME"), 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(requireParameter(libraryParameters, "DESIGN_DESCRIPTION")); - final String sourceBioSampleAccession = bioSampleAccessions.get("SOURCE"); + final String sampleAccession = bioSampleAccessions.get("SOURCE"); designElement .addElement("SAMPLE_DESCRIPTOR") - .addAttribute("accession", sourceBioSampleAccession); + .addAttribute("accession", requireValue(sampleAccession, "BioSamples sample accession")); final Element libraryDescriptorElement = designElement.addElement("LIBRARY_DESCRIPTOR"); + addLibraryDescriptor(libraryDescriptorElement, library, libraryParameters); + addPlatform(experimentElement, sequencingParameters); + } - // 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(); + /** + * 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 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; + } - if (protocolParameters != null && parameterValues != null) { - addLibraryParameters( - libraryDescriptorElement, library, protocolParameters, parameterValues); + 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) { + continue; + } + + final String parameterName = parameterNamesById.get(parameterValue.getCategory().getId()); + if (parameterName == null || parameterName.isBlank()) { + continue; } + + parameterValuesByName.put(parameterName, parameterValue.getValue().getAnnotationValue()); } - // 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"); + return parameterValuesByName; } - /** - * 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 - */ - private void addLibraryParameters( + /** 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 Map libraryParameters) { + addOptionalTextElement( + libraryDescriptorElement, + "LIBRARY_NAME", + firstNonBlank(libraryParameters.get("LIBRARY_NAME"), library.getName())); + libraryDescriptorElement + .addElement("LIBRARY_STRATEGY") + .addText(requireParameter(libraryParameters, "LIBRARY_STRATEGY")); + libraryDescriptorElement + .addElement("LIBRARY_SOURCE") + .addText(requireParameter(libraryParameters, "LIBRARY_SOURCE")); + libraryDescriptorElement + .addElement("LIBRARY_SELECTION") + .addText(requireParameter(libraryParameters, "LIBRARY_SELECTION")); + + final Element libraryLayoutElement = libraryDescriptorElement.addElement("LIBRARY_LAYOUT"); + final String layout = requireParameter(libraryParameters, "LIBRARY_LAYOUT"); + final Element layoutElement = libraryLayoutElement.addElement(layout); + + if ("PAIRED".equals(layout)) { + addOptionalAttribute(layoutElement, "NOMINAL_LENGTH", libraryParameters.get("NOMINAL_LENGTH")); + addOptionalAttribute(layoutElement, "NOMINAL_SDEV", libraryParameters.get("NOMINAL_SDEV")); } - // Add elements in the required order - // 1. LIBRARY_NAME - if (libraryName != null) { - libraryDescriptorElement.addElement("LIBRARY_NAME").addText(libraryName); - } + addOptionalTextElement( + libraryDescriptorElement, + "POOLING_STRATEGY", + libraryParameters.get("POOLING_STRATEGY")); + addOptionalTextElement( + libraryDescriptorElement, + "LIBRARY_CONSTRUCTION_PROTOCOL", + libraryParameters.get("LIBRARY_CONSTRUCTION_PROTOCOL")); + } - // 2. LIBRARY_STRATEGY - if (libraryStrategy != null) { - libraryDescriptorElement.addElement("LIBRARY_STRATEGY").addText(libraryStrategy); - } + /** Populates the ENA PLATFORM block directly from sequencing-process parameters. */ + private void addPlatform( + final Element experimentElement, final Map sequencingParameters) { + final String platform = requireParameter(sequencingParameters, "PLATFORM"); + final String instrumentModel = requireParameter(sequencingParameters, "INSTRUMENT_MODEL"); - // 3. LIBRARY_SOURCE - if (librarySource != null) { - libraryDescriptorElement.addElement("LIBRARY_SOURCE").addText(librarySource); - } + final Element platformElement = experimentElement.addElement("PLATFORM"); + final Element platformTypeElement = platformElement.addElement(platform); + platformTypeElement.addElement("INSTRUMENT_MODEL").addText(instrumentModel); + } - // 4. LIBRARY_SELECTION - if (librarySelection != null) { - libraryDescriptorElement.addElement("LIBRARY_SELECTION").addText(librarySelection); + private void addOptionalTextElement( + final Element parentElement, final String elementName, final String value) { + if (value != null && !value.isBlank()) { + parentElement.addElement(elementName).addText(value); } + } - // 5. LIBRARY_LAYOUT - if (libraryLayout != null) { - final Element libraryLayoutElement = libraryDescriptorElement.addElement("LIBRARY_LAYOUT"); - libraryLayoutElement.addElement(libraryLayout.toUpperCase()); + private void addOptionalAttribute( + final Element element, final String attributeName, final String value) { + if (value != null && !value.isBlank()) { + element.addAttribute(attributeName, value); } } - private boolean isALibraryStrategyParameterName(final String parameterName) { - return parameterName.equalsIgnoreCase("library strategy"); + private String requireParameter( + final Map parameterValues, final String parameterName) { + return requireValue( + parameterValues.get(parameterName), "parameter " + parameterName); + } + + 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 index e92be9a6..0092ddeb 100644 --- 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 @@ -1,50 +1,104 @@ /** Elixir BioHackathon 2022 */ package com.elixir.biohackaton.ISAToSRA.sra.service; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Comment; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Investigation; +import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.dom4j.Element; import org.springframework.stereotype.Service; /** - * Service for creating ENA PROJECT XML elements from ISA-JSON Investigation objects. + * Service for creating ENA PROJECT XML elements from ISA-JSON assay comments. * - *

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: - * - *

+ *

Each assay is treated as the ENA study/project unit so the resulting ENA project alias stays + * aligned with the assay-based receipt path. Project title and description are read directly from + * assay comments using the ENA study field names. */ @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()); + if (investigation == null || investigation.getStudies() == null) { + return; + } + + investigation.getStudies().forEach( + study -> { + if (study.getAssays() == null) { + return; + } + + study + .getAssays() + .forEach( + assay -> createProjectElement( + projectSetElement, study, assay, randomSubmissionIdentifier)); + }); + } + + private void createProjectElement( + final Element projectSetElement, + final Study study, + final Assay assay, + final String randomSubmissionIdentifier) { + final Map assayCommentMap = buildCommentMap(assay.getComments()); + final String assayId = requireField(assay.getId(), "assay @id"); + final String projectTitle = requireField(assayCommentMap.get("STUDY_TITLE"), "STUDY_TITLE"); + final String projectDescription = + firstNonBlank( + assayCommentMap.get("STUDY_DESCRIPTION"), + assayCommentMap.get("STUDY_ABSTRACT"), + study != null ? study.getDescription() : null); + + final Element projectElement = projectSetElement.addElement("PROJECT"); + projectElement.addAttribute("alias", assayId + "-" + randomSubmissionIdentifier); + projectElement.addElement("TITLE").addText(projectTitle); + projectElement.addElement("DESCRIPTION").addText(requireField(projectDescription, "STUDY_DESCRIPTION or STUDY_ABSTRACT")); - // ENA requires SUBMISSION_PROJECT with a project type (e.g., SEQUENCING_PROJECT) final Element submissionProjectElement = projectElement.addElement("SUBMISSION_PROJECT"); submissionProjectElement.addElement("SEQUENCING_PROJECT"); } + + 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 String firstNonBlank(final String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } + + private String requireField(final String value, final String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException( + "Cannot create ENA PROJECT element because " + fieldName + " is missing."); + } + return value; + } } 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..a66a25ba 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,38 @@ /** 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 assay comments. * - *

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: - * - *

+ *

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. Study descriptor values are read directly from assay comments + * using ENA-native field names such as STUDY_TITLE, STUDY_DESCRIPTION, + * STUDY_ABSTRACT, STUDY_TYPE, and new_study_type. */ @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_TITLE", + "STUDY_DESCRIPTION", + "STUDY_ABSTRACT", + "STUDY_TYPE", + "existing_study_type", + "new_study_type"); + public void createENAStudySetElement( final Element webinElement, final List studies, @@ -43,44 +42,139 @@ 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(requireAssayComment(assayCommentMap, "STUDY_TITLE", assayId)); + + addOptionalTextElement( + studyDescriptorElement, "STUDY_DESCRIPTION", assayCommentMap.get("STUDY_DESCRIPTION")); + addOptionalTextElement( + studyDescriptorElement, "STUDY_ABSTRACT", assayCommentMap.get("STUDY_ABSTRACT")); + + 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 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]; + } } 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..f16d2edf 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 @@ -82,6 +82,10 @@ void testCreateENAExperimentSetElement() throws Exception { 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 +94,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 +110,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 +139,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(); 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.json b/test-data/biosamples-input-isa.json index 8e938f2e..1f93bf7e 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -411,9 +411,9 @@ } ] }, - { - "@id": "#protocol/20_20", - "name": "library construction", + { + "@id": "#protocol/20_20", + "name": "library construction", "protocolType": { "annotationValue": "library construction", "termAccession": "", @@ -422,62 +422,70 @@ "description": "", "uri": "", "version": "", - "parameters": [ - { - "@id": "#parameter/349", - "parameterName": { - "annotationValue": "library_construction_protocol", - "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": "insert size", - "termAccession": "", - "termSource": "" - } + "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": [ @@ -491,9 +499,9 @@ } ] }, - { - "@id": "#protocol/21_21", - "name": "nucleic acid sequencing", + { + "@id": "#protocol/21_21", + "name": "nucleic acid sequencing", "protocolType": { "annotationValue": "nucleic acid sequencing", "termAccession": "", @@ -502,14 +510,22 @@ "description": "", "uri": "", "version": "", - "parameters": [ - { - "@id": "#parameter/363", - "parameterName": { - "annotationValue": "sequencing instrument", - "termAccession": "", - "termSource": "" - } + "parameters": [ + { + "@id": "#parameter/362", + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/363", + "parameterName": { + "annotationValue": "INSTRUMENT_MODEL", + "termAccession": "", + "termSource": "" + } } ], "components": [ @@ -601,6 +617,26 @@ { "name": "target_repository", "value": "ena" + }, + { + "name": "STUDY_TITLE", + "value": "Arabidopsis thaliana sequencing project" + }, + { + "name": "STUDY_DESCRIPTION", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_ABSTRACT", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_TYPE", + "value": "Other" + }, + { + "name": "new_study_type", + "value": "Amplicon sequencing" } ], "materials": { @@ -714,7 +750,7 @@ "@id": "#parameter/349" }, "value": { - "annotationValue": "lib prep", + "annotationValue": "PCR amplicon library preparation for Arabidopsis leaf DNA", "termSource": "", "termAccession": "" }, @@ -724,12 +760,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 +790,7 @@ "@id": "#parameter/352" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "GENOMIC", "termSource": "", "termAccession": "" } @@ -754,7 +800,7 @@ "@id": "#parameter/353" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "AMPLICON", "termSource": "", "termAccession": "" } @@ -764,7 +810,7 @@ "@id": "#parameter/354" }, "value": { - "annotationValue": "RT-PCR", + "annotationValue": "PCR", "termSource": "", "termAccession": "" } @@ -816,12 +862,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..d0fce9d8 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-isa.json @@ -306,7 +306,7 @@ "@id": "#characteristic_category/collection_date_323" }, "value": { - "annotationValue": "01/01/2022", + "annotationValue": "2022-01-01", "termSource": "", "termAccession": "" }, @@ -517,7 +517,7 @@ "parameters": [ { "parameterName": { - "annotationValue": "library_construction_protocol", + "annotationValue": "LIBRARY_CONSTRUCTION_PROTOCOL", "termAccession": "", "termSource": "" }, @@ -525,7 +525,15 @@ }, { "parameterName": { - "annotationValue": "design_description", + "annotationValue": "LIBRARY_NAME", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/350" + }, + { + "parameterName": { + "annotationValue": "DESIGN_DESCRIPTION", "termAccession": "", "termSource": "" }, @@ -533,7 +541,7 @@ }, { "parameterName": { - "annotationValue": "library source", + "annotationValue": "LIBRARY_SOURCE", "termAccession": "", "termSource": "" }, @@ -541,7 +549,7 @@ }, { "parameterName": { - "annotationValue": "library strategy", + "annotationValue": "LIBRARY_STRATEGY", "termAccession": "", "termSource": "" }, @@ -549,7 +557,7 @@ }, { "parameterName": { - "annotationValue": "library selection", + "annotationValue": "LIBRARY_SELECTION", "termAccession": "", "termSource": "" }, @@ -557,7 +565,7 @@ }, { "parameterName": { - "annotationValue": "library layout", + "annotationValue": "LIBRARY_LAYOUT", "termAccession": "", "termSource": "" }, @@ -565,7 +573,7 @@ }, { "parameterName": { - "annotationValue": "insert size", + "annotationValue": "NOMINAL_LENGTH", "termAccession": "", "termSource": "" }, @@ -597,7 +605,15 @@ "parameters": [ { "parameterName": { - "annotationValue": "sequencing instrument", + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + }, + "@id": "#parameter/362" + }, + { + "parameterName": { + "annotationValue": "INSTRUMENT_MODEL", "termAccession": "", "termSource": "" }, @@ -643,6 +659,32 @@ ], "assays": [ { + "comments": [ + { + "name": "target_repository", + "value": "ena" + }, + { + "name": "STUDY_TITLE", + "value": "Arabidopsis thaliana sequencing project" + }, + { + "name": "STUDY_DESCRIPTION", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_ABSTRACT", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_TYPE", + "value": "Other" + }, + { + "name": "new_study_type", + "value": "Amplicon sequencing" + } + ], "filename": "a_assays.txt", "measurementType": { "annotationValue": "", @@ -819,7 +861,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": "" }, @@ -834,7 +891,7 @@ "@id": "#parameter/351" }, "value": { - "annotationValue": "Test", + "annotationValue": "Amplicon sequencing of Arabidopsis thaliana leaf DNA.", "termSource": "", "termAccession": "" }, @@ -849,7 +906,7 @@ "@id": "#parameter/352" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "GENOMIC", "termSource": "", "termAccession": "" }, @@ -864,7 +921,7 @@ "@id": "#parameter/353" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "AMPLICON", "termSource": "", "termAccession": "" }, @@ -879,7 +936,7 @@ "@id": "#parameter/354" }, "value": { - "annotationValue": "RT-PCR", + "annotationValue": "PCR", "termSource": "", "termAccession": "" }, @@ -946,12 +1003,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/biosamples-original-isa.json b/test-data/biosamples-original-isa.json index 0554c189..4c6b8d2a 100644 --- a/test-data/biosamples-original-isa.json +++ b/test-data/biosamples-original-isa.json @@ -307,7 +307,7 @@ "@id": "#characteristic_category/collection_date_323" }, "value": { - "annotationValue": "01/01/2022", + "annotationValue": "2022-01-01", "termSource": "", "termAccession": "" }, @@ -502,7 +502,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 +518,7 @@ { "@id": "#parameter/351", "parameterName": { - "annotationValue": "design_description", + "annotationValue": "DESIGN_DESCRIPTION", "termAccession": "", "termSource": "" } @@ -518,7 +526,7 @@ { "@id": "#parameter/352", "parameterName": { - "annotationValue": "library source", + "annotationValue": "LIBRARY_SOURCE", "termAccession": "", "termSource": "" } @@ -526,7 +534,7 @@ { "@id": "#parameter/353", "parameterName": { - "annotationValue": "library strategy", + "annotationValue": "LIBRARY_STRATEGY", "termAccession": "", "termSource": "" } @@ -534,7 +542,7 @@ { "@id": "#parameter/354", "parameterName": { - "annotationValue": "library selection", + "annotationValue": "LIBRARY_SELECTION", "termAccession": "", "termSource": "" } @@ -542,7 +550,7 @@ { "@id": "#parameter/355", "parameterName": { - "annotationValue": "library layout", + "annotationValue": "LIBRARY_LAYOUT", "termAccession": "", "termSource": "" } @@ -550,7 +558,7 @@ { "@id": "#parameter/356", "parameterName": { - "annotationValue": "insert size", + "annotationValue": "NOMINAL_LENGTH", "termAccession": "", "termSource": "" } @@ -579,10 +587,18 @@ "uri": "", "version": "", "parameters": [ + { + "@id": "#parameter/362", + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + } + }, { "@id": "#parameter/363", "parameterName": { - "annotationValue": "sequencing instrument", + "annotationValue": "INSTRUMENT_MODEL", "termAccession": "", "termSource": "" } @@ -673,6 +689,32 @@ } } ], + "comments": [ + { + "name": "target_repository", + "value": "ena" + }, + { + "name": "STUDY_TITLE", + "value": "Arabidopsis thaliana sequencing project" + }, + { + "name": "STUDY_DESCRIPTION", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_ABSTRACT", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_TYPE", + "value": "Other" + }, + { + "name": "new_study_type", + "value": "Amplicon sequencing" + } + ], "materials": { "samples": [ { @@ -804,7 +846,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 +876,7 @@ "@id": "#parameter/351" }, "value": { - "annotationValue": "Test", + "annotationValue": "Amplicon sequencing of Arabidopsis thaliana leaf DNA.", "termSource": "", "termAccession": "" }, @@ -834,7 +891,7 @@ "@id": "#parameter/352" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "GENOMIC", "termSource": "", "termAccession": "" }, @@ -849,7 +906,7 @@ "@id": "#parameter/353" }, "value": { - "annotationValue": "OTHER", + "annotationValue": "AMPLICON", "termSource": "", "termAccession": "" }, @@ -864,7 +921,7 @@ "@id": "#parameter/354" }, "value": { - "annotationValue": "RT-PCR", + "annotationValue": "PCR", "termSource": "", "termAccession": "" }, @@ -930,13 +987,28 @@ "executesProtocol": { "@id": "#protocol/21_21" }, - "parameterValues": [ - { - "category": { - "@id": "#parameter/363" - }, - "value": { - "annotationValue": " MinION", + "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "OXFORD_NANOPORE", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#parameter/363" + }, + "value": { + "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 +} From e3357d8eddd527e4dcc23091bd573cdd1f2d7785 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 19:09:51 +0200 Subject: [PATCH 02/27] remove unecessary code --- .../WebinIsaToXmlSubmissionController.java | 38 ++++--------------- 1 file changed, 7 insertions(+), 31 deletions(-) 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"}} * From 826d09d42d9c10709a278a78b79031e7326569db Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 20:49:38 +0200 Subject: [PATCH 03/27] add a multi experiment example --- test-data/biosamples-input-isa-multi.json | 1625 +++++++++++++++++++++ 1 file changed, 1625 insertions(+) create mode 100644 test-data/biosamples-input-isa-multi.json diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json new file mode 100644 index 00000000..56207527 --- /dev/null +++ b/test-data/biosamples-input-isa-multi.json @@ -0,0 +1,1625 @@ +{ + "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": "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": [ + { + "@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/cell_type_321", + "characteristicType": { + "annotationValue": "cell_type", + "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": "" + } + } + ], + "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/cell_type_321" + }, + "value": { + "annotationValue": "na", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/dev_stage_322" + }, + "value": { + "annotationValue": "budding", + "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": "seed", + "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/cell_type_321" + }, + "value": { + "annotationValue": "root tissue", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/dev_stage_322" + }, + "value": { + "annotationValue": "flowering", + "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": "root", + "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": [], + "factorValues": [] + }, + { + "@id": "#sample/431", + "name": "root 1", + "derivesFrom": [ + { + "@id": "#source/430" + } + ], + "characteristics": [], + "factorValues": [] + } + ] + }, + "protocols": [ + { + "@id": "#protocol/18_10", + "name": "sample collection", + "protocolType": { + "annotationValue": "sample collection", + "termAccession": "", + "termSource": "" + }, + "description": "", + "uri": "", + "version": "", + "parameters": [], + "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": [], + "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": [], + "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": "" + } + }, + { + "@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_TITLE", + "value": "Arabidopsis thaliana sequencing project" + }, + { + "name": "STUDY_DESCRIPTION", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "name": "STUDY_ABSTRACT", + "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + }, + { + "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": [], + "derivesFrom": [ + { + "@id": "#sample/331" + } + ], + "comments": [] + }, + { + "@id": "#other_material/333", + "name": "library 1", + "type": "library name", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_350" + }, + "value": { + "annotationValue": "library 1", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + }, + { + "category": { + "@id": "#characteristic_category/submission_date_358" + }, + "value": { + "annotationValue": "", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + }, + { + "category": { + "@id": "#characteristic_category/status_359" + }, + "value": { + "annotationValue": "", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + }, + { + "category": { + "@id": "#characteristic_category/accession_360" + }, + "value": { + "annotationValue": "", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + } + ], + "derivesFrom": [ + { + "@id": "#other_material/332" + } + ], + "comments": [] + }, + { + "@id": "#other_material/337", + "name": "extract 2", + "type": "library name", + "characteristics": [], + "derivesFrom": [ + { + "@id": "#sample/431" + } + ], + "comments": [] + }, + { + "@id": "#other_material/338", + "name": "library 2", + "type": "library name", + "characteristics": [ + { + "category": { + "@id": "#characteristic_category/Title_350" + }, + "value": { + "annotationValue": "library 2", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + }, + { + "category": { + "@id": "#characteristic_category/submission_date_358" + }, + "value": { + "annotationValue": "", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + }, + { + "category": { + "@id": "#characteristic_category/status_359" + }, + "value": { + "annotationValue": "", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + }, + { + "category": { + "@id": "#characteristic_category/accession_360" + }, + "value": { + "annotationValue": "", + "termAccession": "", + "termSource": "", + "comments": [] + }, + "comments": [] + } + ], + "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": "NovaSeq 6000", + "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": [] + } + ] + } +} \ No newline at end of file From 7ae7d5397f45e162fcbaf2dfe680a5c11ceffb1e Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 20:53:25 +0200 Subject: [PATCH 04/27] correct ilumina model --- test-data/biosamples-input-isa-multi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index 56207527..d812faf4 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.json @@ -1239,7 +1239,7 @@ "@id": "#parameter/363" }, "value": { - "annotationValue": "NovaSeq 6000", + "annotationValue": "Illumina MiSeq", "termAccession": "", "termSource": "", "comments": [] @@ -1622,4 +1622,4 @@ } ] } -} \ No newline at end of file +} From 7e4caaee5f902e7386faa6b932d170d3d4a0defc Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 21:20:38 +0200 Subject: [PATCH 05/27] add support for paired end reads --- .../sra/service/MarsReceiptService.java | 63 ++++++++++- .../sra/service/WebinRunXmlCreator.java | 107 +++++++++++++----- .../ISAToSRA/WebinRunXmlCreatorTest.java | 52 +++++++-- 3 files changed, 179 insertions(+), 43 deletions(-) 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 ffd0765e..d3324dc4 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 @@ -8,6 +8,7 @@ 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.Output; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsError; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsErrorType; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsReceipt; @@ -19,6 +20,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -89,7 +91,7 @@ public MarsReceipt convertReceiptToMars(final Receipt receipt, final IsaJson isa 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); @@ -114,6 +116,65 @@ private ReceiptAccessionsMap getAliasAccessionPairs( }; } + private ReceiptAccessionsMap getRunAliasAccessionPairs( + final List items, final IsaJson isaJson) { + Predicate aliasAccessionPairValidateFn = this::aliasAccessionPairFilter; + final Map accessionMap = new HashMap<>(); + + Optional.ofNullable(items) + .orElse(new ArrayList<>()) + .stream() + .filter(aliasAccessionPairValidateFn) + .forEach( + receiptObject -> { + final String processId = getPreRandomizedAlias(receiptObject); + getDataFileIdsForSequencingProcess(isaJson, processId) + .forEach(dataFileId -> accessionMap.put(dataFileId, receiptObject.getAccession())); + }); + + return new ReceiptAccessionsMap() { + { + isaItemName = DataFile.Fields.id; + this.accessionMap = accessionMap; + } + }; + } + + 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 -> + Optional.ofNullable(process.getOutputs()) + .orElse(new ArrayList<>()) + .stream() + .map(Output::getId) + .filter(id -> id != null && !id.isBlank()) + .map(this::normalizeDataFileId) + .forEach(dataFileIds::add)))); + + return dataFileIds; + } + + private String normalizeDataFileId(final String id) { + return id == null ? null : id.replace("#data_file/", "#data/"); + } + private boolean aliasAccessionPairFilter(ReceiptObject item) { if (item == null) { setMarsReceiptErrors("ENA receipt: Item is NULL"); 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..72d16a45 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,11 @@ package com.elixir.biohackaton.ISAToSRA.sra.service; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; +import java.util.ArrayList; +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 +26,7 @@ public void createENARunSetElement( .getAssays() .forEach( assay -> { + final Set processedSequencingProcesses = new HashSet<>(); if (assay.getDataFiles() != null) { assay .getDataFiles() @@ -34,6 +38,11 @@ public void createENARunSetElement( 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( @@ -43,10 +52,13 @@ public void createENARunSetElement( && experimentSequenceMap.containsKey(library.getId())) { final String experimentId = experimentSequenceMap.get(library.getId()); + final List runDataFiles = + findDataFilesFromProcessOutputs( + sequencingProcess, assay.getDataFiles()); createRunElement( runSetElement, - dataFile, - assay, + sequencingProcess, + runDataFiles, experimentId, randomSubmissionIdentifier); } @@ -112,54 +124,89 @@ private OtherMaterial findLibraryFromProcessInput( return null; } - /** Creates an ENA RUN element from a DataFile. */ + /** Resolves the assay data files referenced by a sequencing process outputs list. */ + private List findDataFilesFromProcessOutputs( + final ProcessSequence process, final List assayDataFiles) { + final List dataFiles = new ArrayList<>(); + if (process.getOutputs() == null || assayDataFiles == null) { + return dataFiles; + } + + for (final Output output : process.getOutputs()) { + if (output.getId() == null) { + continue; + } + + final String normalizedOutputId = normalizeDataFileId(output.getId()); + for (final DataFile dataFile : assayDataFiles) { + if (dataFile.getId() != null + && normalizeDataFileId(dataFile.getId()).equals(normalizedOutputId)) { + dataFiles.add(dataFile); + } + } + } + + return dataFiles; + } + + /** 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/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"); } } From c3898381a624f720e147cb9bd693a66bfa648f7e Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 21:32:52 +0200 Subject: [PATCH 06/27] retuen all run accessions in the receipt --- .../sra/service/MarsReceiptService.java | 88 ++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) 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 d3324dc4..484fc577 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 @@ -9,6 +9,7 @@ import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.IsaJson; 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 com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsError; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsErrorType; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsReceipt; @@ -22,6 +23,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.HashSet; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -120,6 +123,9 @@ private ReceiptAccessionsMap getRunAliasAccessionPairs( final List items, final IsaJson isaJson) { Predicate aliasAccessionPairValidateFn = this::aliasAccessionPairFilter; final Map accessionMap = new HashMap<>(); + final List> sequencingProcessDataFiles = + getSequencingProcessDataFileIdsInSubmissionOrder(isaJson); + int sequencingProcessIndex = 0; Optional.ofNullable(items) .orElse(new ArrayList<>()) @@ -128,8 +134,15 @@ private ReceiptAccessionsMap getRunAliasAccessionPairs( .forEach( receiptObject -> { final String processId = getPreRandomizedAlias(receiptObject); - getDataFileIdsForSequencingProcess(isaJson, processId) - .forEach(dataFileId -> accessionMap.put(dataFileId, receiptObject.getAccession())); + List dataFileIds = getDataFileIdsForSequencingProcess(isaJson, processId); + if (dataFileIds.isEmpty() + && sequencingProcessIndex < sequencingProcessDataFiles.size()) { + dataFileIds = sequencingProcessDataFiles.get(sequencingProcessIndex); + } + + dataFileIds.forEach( + dataFileId -> accessionMap.put(dataFileId, receiptObject.getAccession())); + sequencingProcessIndex++; }); return new ReceiptAccessionsMap() { @@ -171,6 +184,77 @@ private List getDataFileIdsForSequencingProcess( 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 = + findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); + if (sequencingProcess == null || !processedSequencingProcesses.add(sequencingProcess.getId())) { + continue; + } + + final List dataFileIds = new ArrayList<>(); + Optional.ofNullable(sequencingProcess.getOutputs()) + .orElse(new ArrayList<>()) + .stream() + .map(Output::getId) + .filter(id -> id != null && !id.isBlank()) + .map(this::normalizeDataFileId) + .forEach(dataFileIds::add); + sequencingProcessDataFiles.add(dataFileIds); + } + } + + private 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.getOutputs() == null) { + continue; + } + + for (final Output output : process.getOutputs()) { + if (output.getId() == null) { + continue; + } + + if (normalizeDataFileId(output.getId()).equals(normalizedOutputId)) { + return process; + } + } + } + + return null; + } + private String normalizeDataFileId(final String id) { return id == null ? null : id.replace("#data_file/", "#data/"); } From a98589b12e15327fb10ce7bf69419b260e3829ab Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 21:41:07 +0200 Subject: [PATCH 07/27] add support for multiple experiments/samples --- .../sra/service/MarsReceiptService.java | 31 ++-- .../service/WebinExperimentXmlCreator.java | 139 +++++++++++++++++- 2 files changed, 152 insertions(+), 18 deletions(-) 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 484fc577..bc0b0095 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 @@ -127,23 +127,20 @@ private ReceiptAccessionsMap getRunAliasAccessionPairs( getSequencingProcessDataFileIdsInSubmissionOrder(isaJson); int sequencingProcessIndex = 0; - Optional.ofNullable(items) - .orElse(new ArrayList<>()) - .stream() - .filter(aliasAccessionPairValidateFn) - .forEach( - receiptObject -> { - final String processId = getPreRandomizedAlias(receiptObject); - List dataFileIds = getDataFileIdsForSequencingProcess(isaJson, processId); - if (dataFileIds.isEmpty() - && sequencingProcessIndex < sequencingProcessDataFiles.size()) { - dataFileIds = sequencingProcessDataFiles.get(sequencingProcessIndex); - } - - dataFileIds.forEach( - dataFileId -> accessionMap.put(dataFileId, receiptObject.getAccession())); - sequencingProcessIndex++; - }); + 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 -> accessionMap.put(dataFileId, receiptObject.getAccession())); + sequencingProcessIndex++; + } return new ReceiptAccessionsMap() { { 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 65cea49c..fb6e250b 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,7 +2,11 @@ package com.elixir.biohackaton.ISAToSRA.sra.service; import com.elixir.biohackaton.ISAToSRA.receipt.MarsReceiptException; +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.Input; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Materials; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.OtherMaterial; @@ -10,10 +14,13 @@ 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.HashSet; 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; @@ -173,6 +180,7 @@ private void mapExperimentForDataFile( createExperimentElement( root, + study, library, assay, libraryConstructionProcess, @@ -242,6 +250,7 @@ private OtherMaterial findLibraryFromProcessInput( /** Creates one ENA EXPERIMENT element using ENA-native assay/process parameter names. */ private void createExperimentElement( final Element root, + final Study study, final OtherMaterial library, final com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, final ProcessSequence libraryConstructionProcess, @@ -269,7 +278,7 @@ private void createExperimentElement( .addElement("DESIGN_DESCRIPTION") .addText(requireParameter(libraryParameters, "DESIGN_DESCRIPTION")); - final String sampleAccession = bioSampleAccessions.get("SOURCE"); + final String sampleAccession = resolveSampleAccessionForLibrary(study, assay, library); designElement .addElement("SAMPLE_DESCRIPTOR") .addAttribute("accession", requireValue(sampleAccession, "BioSamples sample accession")); @@ -279,6 +288,134 @@ private void createExperimentElement( addPlatform(experimentElement, sequencingParameters); } + /** + * Resolves the BioSamples accession for the biological sample that the library derives from. + */ + private String resolveSampleAccessionForLibrary( + final Study study, + final com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, + final OtherMaterial library) { + if (study == null || study.getMaterials() == null || study.getMaterials().getSamples() == null) { + return ""; + } + + final Map samplesById = new HashMap<>(); + for (final Sample sample : study.getMaterials().getSamples()) { + if (sample != null && sample.getId() != null) { + samplesById.put(sample.getId(), sample); + } + } + + final Map otherMaterialsById = new HashMap<>(); + if (assay.getMaterials() != null && assay.getMaterials().getOtherMaterials() != null) { + for (final OtherMaterial otherMaterial : assay.getMaterials().getOtherMaterials()) { + if (otherMaterial != null && otherMaterial.getId() != null) { + otherMaterialsById.put(otherMaterial.getId(), otherMaterial); + } + } + } + + final Sample sample = + findSampleForMaterialId(library.getId(), samplesById, otherMaterialsById, new HashSet<>()); + if (sample == null) { + return ""; + } + + final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study); + return getCharacteristicAnnotation(sample.getCharacteristics(), characteristicKeyLookup); + } + + 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 Map keyLookup = new HashMap<>(); + + if (study == null || study.characteristicCategories == null) { + return keyLookup; + } + + for (CharacteristicCategory characteristicCategory : study.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); + } + + return keyLookup; + } + + private String getCharacteristicAnnotation( + final List characteristics, final Map characteristicKeyLookup) { + if (characteristics == null) { + return ""; + } + + for (Characteristic characteristic : characteristics) { + if (characteristic.category == null) { + continue; + } + + final Category category = characteristic.category; + final String characteristicName = + category.id != null ? characteristicKeyLookup.get(category.id) : null; + final boolean accessionCategoryNameMatches = + "accession".equalsIgnoreCase(characteristicName) + || (category.characteristicType != null + && "accession".equalsIgnoreCase(category.characteristicType.annotationValue)); + final boolean accessionCategoryIdMatches = + !accessionCategoryNameMatches + && category.id != null + && category.id.startsWith("#characteristic_category/accession"); + + if ((accessionCategoryIdMatches || accessionCategoryNameMatches) + && characteristic.value != null) { + return characteristic.value.annotationValue; + } + } + + return ""; + } + /** * 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. From c01abddbc7491980cbc955b5a91ef56cdd24e5e6 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 21:49:05 +0200 Subject: [PATCH 08/27] add receipt to logging --- .../ISAToSRA/controller/WebinIsaToXmlSubmissionController.java | 2 ++ 1 file changed, 2 insertions(+) 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 f691dd17..777a0e9c 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 @@ -159,6 +159,8 @@ public String performSubmissionToEna( final String receiptXml = webinHttpSubmissionService.performWebinSubmission( webinUserName, document.asXML(), webinPassword); + System.out.println("\n=== Raw ENA Receipt XML ==="); + System.out.println(receiptXml); // Step 6: Convert ENA XML receipt to JSON final Receipt receiptJson = receiptConversionService.readReceiptXml(receiptXml); From 5b938cd944a0b82866f785cda44112de2f0e0f1d Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 22:03:26 +0200 Subject: [PATCH 09/27] add more logs --- .../ISAToSRA/sra/service/MarsReceiptService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 bc0b0095..9ddaeeed 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 @@ -85,6 +85,15 @@ public void setMarsReceiptErrors(MarsError... errors) { * https://github.com/elixir-europe/MARS/blob/refactor/repository-services/repository-api.md#response */ public MarsReceipt convertReceiptToMars(final Receipt receipt, final IsaJson isaJson) { + System.out.println( + "=== Parsed ENA receipt counts === studies=" + + Optional.ofNullable(receipt.getStudies()).orElse(new ArrayList<>()).size() + + ", projects=" + + Optional.ofNullable(receipt.getProjects()).orElse(new ArrayList<>()).size() + + ", experiments=" + + Optional.ofNullable(receipt.getExperiments()).orElse(new ArrayList<>()).size() + + ", runs=" + + Optional.ofNullable(receipt.getRuns()).orElse(new ArrayList<>()).size()); buildMarsReceipt( getAliasAccessionPairs( // ENA study/project aliases are assay-based, so the returned accession path points to @@ -142,6 +151,8 @@ private ReceiptAccessionsMap getRunAliasAccessionPairs( sequencingProcessIndex++; } + System.out.println("=== ENA run accession map === " + accessionMap); + return new ReceiptAccessionsMap() { { isaItemName = DataFile.Fields.id; From c6a3b847a807db8e87d0f8aff85062de1a940f9c Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 22:09:36 +0200 Subject: [PATCH 10/27] MORE! --- .../biohackaton/ISAToSRA/receipt/MarsReceiptProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 257ce61e..b29ecf2b 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 @@ -210,6 +210,11 @@ protected void setMarsAccessions( ReceiptAccessionMap dataFileAccessionMap = getAccessionMapEntry( dataFilesAccessionsMap, dataFile, marsMessage); if (dataFileAccessionMap.accession != null) { + System.out.println( + "=== Adding ENA run accession to Mars receipt === " + + dataFileAccessionMap.isaFieldValue + + " -> " + + dataFileAccessionMap.accession); marsAccessions.add(getDataFileMarsAccession( studyAccessionMap, assay.id, @@ -221,6 +226,7 @@ protected void setMarsAccessions( } } }); + System.out.println("=== Final Mars accession count === " + marsAccessions.size()); } // ------------------------- From 053862fa7cdef27f49369e7a4ece597e7644251b Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 22:15:41 +0200 Subject: [PATCH 11/27] final logging --- .../biohackaton/ISAToSRA/receipt/MarsReceiptProvider.java | 5 +++++ 1 file changed, 5 insertions(+) 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 b29ecf2b..7c12ec62 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 @@ -209,6 +209,11 @@ protected void setMarsAccessions( dataFile -> { ReceiptAccessionMap dataFileAccessionMap = getAccessionMapEntry( dataFilesAccessionsMap, dataFile, marsMessage); + System.out.println( + "=== Data file lookup === id=" + + dataFileAccessionMap.isaFieldValue + + ", accession=" + + dataFileAccessionMap.accession); if (dataFileAccessionMap.accession != null) { System.out.println( "=== Adding ENA run accession to Mars receipt === " From 96f76a7fb9e7abbdf241cc8369015dfce2e45cd5 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Wed, 27 May 2026 22:22:55 +0200 Subject: [PATCH 12/27] fix run accession mapping --- .../WebinIsaToXmlSubmissionController.java | 2 -- .../sra/service/MarsReceiptService.java | 18 ++++-------------- .../ISAToSRA/receipt/MarsReceiptProvider.java | 11 ----------- 3 files changed, 4 insertions(+), 27 deletions(-) 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 777a0e9c..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 @@ -159,8 +159,6 @@ public String performSubmissionToEna( final String receiptXml = webinHttpSubmissionService.performWebinSubmission( webinUserName, document.asXML(), webinPassword); - System.out.println("\n=== Raw ENA Receipt XML ==="); - System.out.println(receiptXml); // Step 6: Convert ENA XML receipt to JSON final Receipt receiptJson = receiptConversionService.readReceiptXml(receiptXml); 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 9ddaeeed..c3b245cf 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 @@ -85,15 +85,6 @@ public void setMarsReceiptErrors(MarsError... errors) { * https://github.com/elixir-europe/MARS/blob/refactor/repository-services/repository-api.md#response */ public MarsReceipt convertReceiptToMars(final Receipt receipt, final IsaJson isaJson) { - System.out.println( - "=== Parsed ENA receipt counts === studies=" - + Optional.ofNullable(receipt.getStudies()).orElse(new ArrayList<>()).size() - + ", projects=" - + Optional.ofNullable(receipt.getProjects()).orElse(new ArrayList<>()).size() - + ", experiments=" - + Optional.ofNullable(receipt.getExperiments()).orElse(new ArrayList<>()).size() - + ", runs=" - + Optional.ofNullable(receipt.getRuns()).orElse(new ArrayList<>()).size()); buildMarsReceipt( getAliasAccessionPairs( // ENA study/project aliases are assay-based, so the returned accession path points to @@ -131,7 +122,7 @@ private ReceiptAccessionsMap getAliasAccessionPairs( private ReceiptAccessionsMap getRunAliasAccessionPairs( final List items, final IsaJson isaJson) { Predicate aliasAccessionPairValidateFn = this::aliasAccessionPairFilter; - final Map accessionMap = new HashMap<>(); + final Map runAccessionMap = new HashMap<>(); final List> sequencingProcessDataFiles = getSequencingProcessDataFileIdsInSubmissionOrder(isaJson); int sequencingProcessIndex = 0; @@ -147,16 +138,15 @@ private ReceiptAccessionsMap getRunAliasAccessionPairs( dataFileIds = sequencingProcessDataFiles.get(sequencingProcessIndex); } - dataFileIds.forEach(dataFileId -> accessionMap.put(dataFileId, receiptObject.getAccession())); + dataFileIds.forEach( + dataFileId -> runAccessionMap.put(dataFileId, receiptObject.getAccession())); sequencingProcessIndex++; } - System.out.println("=== ENA run accession map === " + accessionMap); - return new ReceiptAccessionsMap() { { isaItemName = DataFile.Fields.id; - this.accessionMap = accessionMap; + this.accessionMap = new HashMap<>(runAccessionMap); } }; } 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 7c12ec62..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 @@ -209,17 +209,7 @@ protected void setMarsAccessions( dataFile -> { ReceiptAccessionMap dataFileAccessionMap = getAccessionMapEntry( dataFilesAccessionsMap, dataFile, marsMessage); - System.out.println( - "=== Data file lookup === id=" - + dataFileAccessionMap.isaFieldValue - + ", accession=" - + dataFileAccessionMap.accession); if (dataFileAccessionMap.accession != null) { - System.out.println( - "=== Adding ENA run accession to Mars receipt === " - + dataFileAccessionMap.isaFieldValue - + " -> " - + dataFileAccessionMap.accession); marsAccessions.add(getDataFileMarsAccession( studyAccessionMap, assay.id, @@ -231,7 +221,6 @@ protected void setMarsAccessions( } } }); - System.out.println("=== Final Mars accession count === " + marsAccessions.size()); } // ------------------------- From e54a397ae49cd9d9270ed000267bea26ace8e29f Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 5 Jun 2026 12:09:57 +0200 Subject: [PATCH 13/27] read study title and descr from study + remove unused project xml generator --- .../sra/service/WebinProjectXmlCreator.java | 104 ------------------ .../sra/service/WebinStudyXmlCreator.java | 40 ++++--- test-data/biosamples-input-isa-multi.json | 14 +-- test-data/biosamples-input-isa.json | 14 +-- test-data/biosamples-modified-isa.json | 14 +-- test-data/biosamples-original-isa.json | 14 +-- 6 files changed, 36 insertions(+), 164 deletions(-) delete mode 100644 repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinProjectXmlCreator.java 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 0092ddeb..00000000 --- a/repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/WebinProjectXmlCreator.java +++ /dev/null @@ -1,104 +0,0 @@ -/** Elixir BioHackathon 2022 */ -package com.elixir.biohackaton.ISAToSRA.sra.service; - -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay; -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Comment; -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Investigation; -import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Study; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.dom4j.Element; -import org.springframework.stereotype.Service; - -/** - * Service for creating ENA PROJECT XML elements from ISA-JSON assay comments. - * - *

    Each assay is treated as the ENA study/project unit so the resulting ENA project alias stays - * aligned with the assay-based receipt path. Project title and description are read directly from - * assay comments using the ENA study field names. - */ -@Service -public class WebinProjectXmlCreator { - public void createENAProjectSetElement( - final Element webinElement, - final Investigation investigation, - final String randomSubmissionIdentifier) { - final Element projectSetElement = webinElement.addElement("PROJECT_SET"); - - if (investigation == null || investigation.getStudies() == null) { - return; - } - - investigation.getStudies().forEach( - study -> { - if (study.getAssays() == null) { - return; - } - - study - .getAssays() - .forEach( - assay -> createProjectElement( - projectSetElement, study, assay, randomSubmissionIdentifier)); - }); - } - - private void createProjectElement( - final Element projectSetElement, - final Study study, - final Assay assay, - final String randomSubmissionIdentifier) { - final Map assayCommentMap = buildCommentMap(assay.getComments()); - final String assayId = requireField(assay.getId(), "assay @id"); - final String projectTitle = requireField(assayCommentMap.get("STUDY_TITLE"), "STUDY_TITLE"); - final String projectDescription = - firstNonBlank( - assayCommentMap.get("STUDY_DESCRIPTION"), - assayCommentMap.get("STUDY_ABSTRACT"), - study != null ? study.getDescription() : null); - - final Element projectElement = projectSetElement.addElement("PROJECT"); - projectElement.addAttribute("alias", assayId + "-" + randomSubmissionIdentifier); - projectElement.addElement("TITLE").addText(projectTitle); - projectElement.addElement("DESCRIPTION").addText(requireField(projectDescription, "STUDY_DESCRIPTION or STUDY_ABSTRACT")); - - final Element submissionProjectElement = projectElement.addElement("SUBMISSION_PROJECT"); - submissionProjectElement.addElement("SEQUENCING_PROJECT"); - } - - 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 String firstNonBlank(final String... values) { - for (String value : values) { - if (value != null && !value.isBlank()) { - return value; - } - } - return null; - } - - private String requireField(final String value, final String fieldName) { - if (value == null || value.isBlank()) { - throw new IllegalArgumentException( - "Cannot create ENA PROJECT element because " + fieldName + " is missing."); - } - return value; - } -} 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 a66a25ba..e4e2c569 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 @@ -13,12 +13,12 @@ import org.springframework.stereotype.Service; /** - * Service for creating ENA STUDY XML elements from ISA-JSON assay comments. + * Service for creating ENA STUDY XML elements from ISA-JSON. * *

    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. Study descriptor values are read directly from assay comments - * using ENA-native field names such as STUDY_TITLE, STUDY_DESCRIPTION, - * STUDY_ABSTRACT, STUDY_TYPE, and new_study_type. + * 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 @@ -26,8 +26,6 @@ public class WebinStudyXmlCreator { private static final Set RESERVED_ASSAY_COMMENT_NAMES = Set.of( "target_repository", - "STUDY_TITLE", - "STUDY_DESCRIPTION", "STUDY_ABSTRACT", "STUDY_TYPE", "existing_study_type", @@ -67,17 +65,21 @@ private void createStudyElement( final String assayId = requireAssayField(assay.getId(), "assay @id"); final Element studyElement = - studySetElement.addElement("STUDY").addAttribute("alias", assayId + "-" + randomSubmissionIdentifier); + studySetElement + .addElement("STUDY") + .addAttribute("alias", assayId + "-" + randomSubmissionIdentifier); final Element studyDescriptorElement = studyElement.addElement("DESCRIPTOR"); studyDescriptorElement .addElement("STUDY_TITLE") - .addText(requireAssayComment(assayCommentMap, "STUDY_TITLE", assayId)); + .addText(requireStudyField(study != null ? study.getTitle() : null, "study title", assayId)); addOptionalTextElement( - studyDescriptorElement, "STUDY_DESCRIPTION", assayCommentMap.get("STUDY_DESCRIPTION")); + studyDescriptorElement, "STUDY_DESCRIPTION", study != null ? study.getDescription() : null); addOptionalTextElement( - studyDescriptorElement, "STUDY_ABSTRACT", assayCommentMap.get("STUDY_ABSTRACT")); + studyDescriptorElement, + "STUDY_ABSTRACT", + firstNonBlank(assayCommentMap.get("STUDY_ABSTRACT"), study != null ? study.getDescription() : null)); final Element studyTypeElement = studyDescriptorElement.addElement("STUDY_TYPE"); studyTypeElement.addAttribute( @@ -147,14 +149,11 @@ private void addOptionalTextElement( } } - private String requireAssayComment( - final Map assayCommentMap, - final String commentName, - final String assayId) { - final String value = assayCommentMap.get(commentName); + private String requireStudyField( + final String value, final String fieldName, final String assayId) { if (value == null || value.isBlank()) { throw new IllegalArgumentException( - "Assay " + assayId + " is missing required ENA study comment " + commentName + "."); + "Assay " + assayId + " cannot create an ENA STUDY because the " + fieldName + " is missing."); } return value; } @@ -177,4 +176,13 @@ private String firstNonBlankCommentName( } 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/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index d812faf4..ba98ce85 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.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", @@ -828,17 +828,9 @@ "name": "target_repository", "value": "ena" }, - { - "name": "STUDY_TITLE", - "value": "Arabidopsis thaliana sequencing project" - }, - { - "name": "STUDY_DESCRIPTION", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." - }, { "name": "STUDY_ABSTRACT", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + "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", diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index 1f93bf7e..5585775d 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", @@ -618,17 +618,9 @@ "name": "target_repository", "value": "ena" }, - { - "name": "STUDY_TITLE", - "value": "Arabidopsis thaliana sequencing project" - }, - { - "name": "STUDY_DESCRIPTION", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." - }, { "name": "STUDY_ABSTRACT", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + "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", diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index d0fce9d8..708c0873 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-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", @@ -664,17 +664,9 @@ "name": "target_repository", "value": "ena" }, - { - "name": "STUDY_TITLE", - "value": "Arabidopsis thaliana sequencing project" - }, - { - "name": "STUDY_DESCRIPTION", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." - }, { "name": "STUDY_ABSTRACT", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + "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", diff --git a/test-data/biosamples-original-isa.json b/test-data/biosamples-original-isa.json index 4c6b8d2a..fc6bc10d 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", @@ -694,17 +694,9 @@ "name": "target_repository", "value": "ena" }, - { - "name": "STUDY_TITLE", - "value": "Arabidopsis thaliana sequencing project" - }, - { - "name": "STUDY_DESCRIPTION", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." - }, { "name": "STUDY_ABSTRACT", - "value": "Nucleic acid sequencing and metabolomics and proteomics of Arabidopsis thaliana in specific experimental conditions to test a specific hypothesis." + "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", From 5e719c29e2fb9728e6cc450cb5d91812e952f1d3 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 5 Jun 2026 13:14:05 +0200 Subject: [PATCH 14/27] add missing function --- .../ISAToSRA/sra/service/WebinStudyXmlCreator.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 e4e2c569..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 @@ -158,6 +158,18 @@ private String requireStudyField( 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( From 9752f656724c9ed53190ac37836e60d42d2f3fd6 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 5 Jun 2026 13:23:50 +0200 Subject: [PATCH 15/27] align all example files --- test-data/biosamples-original-isa-no-accesion-char.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/biosamples-original-isa-no-accesion-char.json b/test-data/biosamples-original-isa-no-accesion-char.json index 77e786ff..0550083d 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", From 717da18ab5794552d7dec064e5069d0dc60cec88 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 5 Jun 2026 13:26:39 +0200 Subject: [PATCH 16/27] format ISA JSON --- test-data/biosamples-input-isa-multi.json | 2 +- test-data/biosamples-input-isa.json | 174 +- test-data/biosamples-modified-isa.json | 2084 ++++++++--------- ...samples-original-isa-no-accesion-char.json | 6 +- 4 files changed, 1133 insertions(+), 1133 deletions(-) diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index ba98ce85..2207ee6a 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.json @@ -1614,4 +1614,4 @@ } ] } -} +} \ No newline at end of file diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index 5585775d..f5aa1c17 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -411,9 +411,9 @@ } ] }, - { - "@id": "#protocol/20_20", - "name": "library construction", + { + "@id": "#protocol/20_20", + "name": "library construction", "protocolType": { "annotationValue": "library construction", "termAccession": "", @@ -422,70 +422,70 @@ "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": "" - } + "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": [ @@ -499,9 +499,9 @@ } ] }, - { - "@id": "#protocol/21_21", - "name": "nucleic acid sequencing", + { + "@id": "#protocol/21_21", + "name": "nucleic acid sequencing", "protocolType": { "annotationValue": "nucleic acid sequencing", "termAccession": "", @@ -510,22 +510,22 @@ "description": "", "uri": "", "version": "", - "parameters": [ - { - "@id": "#parameter/362", - "parameterName": { - "annotationValue": "PLATFORM", - "termAccession": "", - "termSource": "" - } - }, - { - "@id": "#parameter/363", - "parameterName": { - "annotationValue": "INSTRUMENT_MODEL", - "termAccession": "", - "termSource": "" - } + "parameters": [ + { + "@id": "#parameter/362", + "parameterName": { + "annotationValue": "PLATFORM", + "termAccession": "", + "termSource": "" + } + }, + { + "@id": "#parameter/363", + "parameterName": { + "annotationValue": "INSTRUMENT_MODEL", + "termAccession": "", + "termSource": "" + } } ], "components": [ @@ -939,4 +939,4 @@ } ] } -} +} \ No newline at end of file diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index 708c0873..2ae495e4 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": "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": [ - { + "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,1004 +37,1050 @@ "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": "2022-01-01", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/isolation_source_324" - }, - "value": { - "annotationValue": "seed", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/collected_by_325" - }, - "value": { - "annotationValue": "Bob", - "termSource": "", - "termAccession": "" + } + ], + "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": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/geographic_location_(country_and/or_sea)_326" + "@id": "#characteristic_category/Title_317" + }, + { + "characteristicType": { + "annotationValue": "Description", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "Belgium", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/Description_318" + }, + { + "characteristicType": { + "annotationValue": "tax_id", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/submission_date_327" + "@id": "#characteristic_category/tax_id_319" + }, + { + "characteristicType": { + "annotationValue": "organism", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/organism_320" + }, + { + "characteristicType": { + "annotationValue": "cell_type", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/status_328" + "@id": "#characteristic_category/cell_type_321" + }, + { + "characteristicType": { + "annotationValue": "dev_stage", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" + "@id": "#characteristic_category/dev_stage_322" + }, + { + "characteristicType": { + "annotationValue": "collection_date", + "termAccession": "", + "termSource": "" }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#characteristic_category/accession_329" + "@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/accession" + "@id": "#characteristic_category/collected_by_325" + }, + { + "characteristicType": { + "annotationValue": "geographic location (country and/or sea)", + "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/geographic_location_(country_and/or_sea)_326" + }, + { + "characteristicType": { + "annotationValue": "submission date", + "termAccession": "", + "termSource": "" }, - "value": { - "annotationValue": "SAMEA130788489" - } - } - ], - "factorValues": [ - { - "category": { - "@id": "" + "@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": [] - } - } - ], - "@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": "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": "" - }, - "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" - } - ], - "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": [ - { - "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": "" - }, - "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/accession_329" + } ], "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/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": "2022-01-01", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/isolation_source_324" + }, + "value": { + "annotationValue": "seed", + "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/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": "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": [] - } + ] + }, + "protocols": [ + { + "name": "sample collection", + "protocolType": { + "annotationValue": "sample collection", + "termAccession": "", + "termSource": "" }, - { - "category": { - "@id": "#parameter/353" - }, - "value": { - "annotationValue": "AMPLICON", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } + "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": "" }, - { - "category": { - "@id": "#parameter/354" - }, - "value": { - "annotationValue": "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/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" + "processSequence": [ + { + "name": "", + "executesProtocol": { + "@id": "#protocol/18_10" }, - { - "name": "file checksum", - "value": "9840f585055afc37de353706fd31a377" + "parameterValues": [], + "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" + }, + { + "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" + } + ], + "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" + } + ] }, - { - "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": [] + } + ] + } +} \ No newline at end of file diff --git a/test-data/biosamples-original-isa-no-accesion-char.json b/test-data/biosamples-original-isa-no-accesion-char.json index 0550083d..3587df33 100644 --- a/test-data/biosamples-original-isa-no-accesion-char.json +++ b/test-data/biosamples-original-isa-no-accesion-char.json @@ -652,8 +652,8 @@ ], "comments": [ { - "name": "target_repository", - "value": "ena" + "name": "target_repository", + "value": "ena" } ], "materials": { @@ -985,4 +985,4 @@ } ] } -} +} \ No newline at end of file From f0f61043a99353588723f73813d57723e3cbaf65 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 5 Jun 2026 13:32:33 +0200 Subject: [PATCH 17/27] fix end of lines --- test-data/biosamples-input-isa-multi.json | 2 +- test-data/biosamples-input-isa.json | 2 +- test-data/biosamples-modified-isa.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index 2207ee6a..ba98ce85 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.json @@ -1614,4 +1614,4 @@ } ] } -} \ No newline at end of file +} diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index f5aa1c17..63d9b89c 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -939,4 +939,4 @@ } ] } -} \ No newline at end of file +} diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index 2ae495e4..1970fb9a 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-isa.json @@ -1083,4 +1083,4 @@ } ] } -} \ No newline at end of file +} From 057df32ff5f176a2a00daa61572aac87187b73da Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 13:58:49 +0200 Subject: [PATCH 18/27] Read also other materials --- .../service/WebinExperimentXmlCreator.java | 316 +++++++++++++++--- .../WebinExperimentXmlCreatorTest.java | 76 +++-- test-data/biosamples-input-isa-multi.json | 146 ++------ test-data/biosamples-input-isa.json | 80 +---- test-data/biosamples-modified-isa.json | 98 +----- ...samples-original-isa-no-accesion-char.json | 102 +----- test-data/biosamples-original-isa.json | 142 ++------ 7 files changed, 416 insertions(+), 544 deletions(-) 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 fb6e250b..062b3855 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,6 +2,7 @@ package com.elixir.biohackaton.ISAToSRA.sra.service; import com.elixir.biohackaton.ISAToSRA.receipt.MarsReceiptException; +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; @@ -16,9 +17,11 @@ 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.HashSet; +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; @@ -156,7 +159,7 @@ private void mapExperimentForDataFile( final String randomSubmissionIdentifier, final Map experimentSequence, final Study study, - final com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, + final Assay assay, final DataFile dataFile) { final ProcessSequence sequencingProcess = findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); @@ -175,6 +178,8 @@ private void mapExperimentForDataFile( final String experimentId = library.getId() + "-" + randomSubmissionIdentifier; experimentSequence.put(library.getId(), experimentId); + final List materialLineage = + findMaterialLineageToSample(library, assay.getMaterials()); final ProcessSequence libraryConstructionProcess = findProcessByOutputId(assay.getProcessSequence(), library.getId()); @@ -182,6 +187,7 @@ private void mapExperimentForDataFile( root, study, library, + materialLineage, assay, libraryConstructionProcess, sequencingProcess, @@ -247,12 +253,66 @@ private OtherMaterial findLibraryFromProcessInput( return null; } + private List findMaterialLineageToSample( + final OtherMaterial material, final Materials materials) { + final List materialLineage = new ArrayList<>(); + final Map otherMaterialsById = buildOtherMaterialsById(materials); + + collectMaterialLineage(material, otherMaterialsById, materialLineage, new HashSet<>()); + + return materialLineage; + } + + private 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; + } + + 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; + } + + 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); + } + } + /** Creates one ENA EXPERIMENT element using ENA-native assay/process parameter names. */ private void createExperimentElement( final Element root, final Study study, final OtherMaterial library, - final com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, + final List materialLineage, + final Assay assay, final ProcessSequence libraryConstructionProcess, final ProcessSequence sequencingProcess, final Map> protocolToParameterNameMap, @@ -263,12 +323,22 @@ private void createExperimentElement( extractParameterValues(libraryConstructionProcess, protocolToParameterNameMap); final Map sequencingParameters = extractParameterValues(sequencingProcess, protocolToParameterNameMap); + final Map materialCharacteristics = + extractMaterialCharacteristicValues( + materialLineage, buildCharacteristicKeyLookup(study, assay)); final Element experimentElement = root.addElement("EXPERIMENT"); experimentElement.addAttribute("alias", experimentId); experimentElement .addElement("TITLE") - .addText(firstNonBlank(libraryParameters.get("TITLE"), libraryParameters.get("LIBRARY_NAME"), library.getName())); + .addText( + firstNonBlank( + getMetadataValue(libraryParameters, "EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), + getMetadataValue( + sequencingParameters, "EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), + getMetadataValue( + materialCharacteristics, "EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), + library.getName())); experimentElement .addElement("STUDY_REF") .addAttribute("refname", assay.getId() + "-" + randomSubmissionIdentifier); @@ -276,7 +346,9 @@ private void createExperimentElement( final Element designElement = experimentElement.addElement("DESIGN"); designElement .addElement("DESIGN_DESCRIPTION") - .addText(requireParameter(libraryParameters, "DESIGN_DESCRIPTION")); + .addText( + requireMetadataValue( + libraryParameters, materialCharacteristics, "DESIGN_DESCRIPTION")); final String sampleAccession = resolveSampleAccessionForLibrary(study, assay, library); designElement @@ -284,8 +356,9 @@ private void createExperimentElement( .addAttribute("accession", requireValue(sampleAccession, "BioSamples sample accession")); final Element libraryDescriptorElement = designElement.addElement("LIBRARY_DESCRIPTOR"); - addLibraryDescriptor(libraryDescriptorElement, library, libraryParameters); - addPlatform(experimentElement, sequencingParameters); + addLibraryDescriptor( + libraryDescriptorElement, library, libraryParameters, materialCharacteristics); + addPlatform(experimentElement, sequencingParameters, materialCharacteristics); } /** @@ -293,7 +366,7 @@ private void createExperimentElement( */ private String resolveSampleAccessionForLibrary( final Study study, - final com.elixir.biohackaton.ISAToSRA.receipt.isamodel.Assay assay, + final Assay assay, final OtherMaterial library) { if (study == null || study.getMaterials() == null || study.getMaterials().getSamples() == null) { return ""; @@ -306,14 +379,8 @@ private String resolveSampleAccessionForLibrary( } } - final Map otherMaterialsById = new HashMap<>(); - if (assay.getMaterials() != null && assay.getMaterials().getOtherMaterials() != null) { - for (final OtherMaterial otherMaterial : assay.getMaterials().getOtherMaterials()) { - if (otherMaterial != null && otherMaterial.getId() != null) { - otherMaterialsById.put(otherMaterial.getId(), otherMaterial); - } - } - } + final Map otherMaterialsById = + buildOtherMaterialsById(assay.getMaterials()); final Sample sample = findSampleForMaterialId(library.getId(), samplesById, otherMaterialsById, new HashSet<>()); @@ -321,7 +388,7 @@ private String resolveSampleAccessionForLibrary( return ""; } - final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study); + final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study, assay); return getCharacteristicAnnotation(sample.getCharacteristics(), characteristicKeyLookup); } @@ -360,14 +427,28 @@ private Sample findSampleForMaterialId( return null; } - private Map buildCharacteristicKeyLookup(final Study study) { + private Map buildCharacteristicKeyLookup(final Study study, final Assay assay) { final Map keyLookup = new HashMap<>(); - if (study == null || study.characteristicCategories == null) { - return keyLookup; + 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 : study.characteristicCategories) { + for (CharacteristicCategory characteristicCategory : characteristicCategories) { if (characteristicCategory == null || characteristicCategory.id == null || characteristicCategory.characteristicType == null @@ -380,8 +461,6 @@ private Map buildCharacteristicKeyLookup(final Study study) { characteristicCategory.id, characteristicCategory.characteristicType.annotationValue); } - - return keyLookup; } private String getCharacteristicAnnotation( @@ -395,20 +474,10 @@ private String getCharacteristicAnnotation( continue; } - final Category category = characteristic.category; final String characteristicName = - category.id != null ? characteristicKeyLookup.get(category.id) : null; - final boolean accessionCategoryNameMatches = - "accession".equalsIgnoreCase(characteristicName) - || (category.characteristicType != null - && "accession".equalsIgnoreCase(category.characteristicType.annotationValue)); - final boolean accessionCategoryIdMatches = - !accessionCategoryNameMatches - && category.id != null - && category.id.startsWith("#characteristic_category/accession"); - - if ((accessionCategoryIdMatches || accessionCategoryNameMatches) - && characteristic.value != null) { + getCharacteristicName(characteristic.category, characteristicKeyLookup); + + if (metadataKeyMatches(characteristicName, "accession") && characteristic.value != null) { return characteristic.value.annotationValue; } } @@ -416,6 +485,77 @@ private String getCharacteristicAnnotation( 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]+$", ""); + } + /** * 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. @@ -463,45 +603,64 @@ private Map extractParameterValues( private void addLibraryDescriptor( final Element libraryDescriptorElement, final OtherMaterial library, - final Map libraryParameters) { + final Map libraryParameters, + final Map materialCharacteristics) { addOptionalTextElement( libraryDescriptorElement, "LIBRARY_NAME", - firstNonBlank(libraryParameters.get("LIBRARY_NAME"), library.getName())); + firstNonBlank( + getMetadataValue(libraryParameters, "LIBRARY_NAME", "LIBRARY NAME"), + getMetadataValue(materialCharacteristics, "LIBRARY_NAME", "LIBRARY NAME"), + library.getName())); libraryDescriptorElement .addElement("LIBRARY_STRATEGY") - .addText(requireParameter(libraryParameters, "LIBRARY_STRATEGY")); + .addText( + requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_STRATEGY")); libraryDescriptorElement .addElement("LIBRARY_SOURCE") - .addText(requireParameter(libraryParameters, "LIBRARY_SOURCE")); + .addText( + requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_SOURCE")); libraryDescriptorElement .addElement("LIBRARY_SELECTION") - .addText(requireParameter(libraryParameters, "LIBRARY_SELECTION")); + .addText( + requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_SELECTION")); final Element libraryLayoutElement = libraryDescriptorElement.addElement("LIBRARY_LAYOUT"); - final String layout = requireParameter(libraryParameters, "LIBRARY_LAYOUT"); + final String layout = + requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_LAYOUT"); final Element layoutElement = libraryLayoutElement.addElement(layout); if ("PAIRED".equals(layout)) { - addOptionalAttribute(layoutElement, "NOMINAL_LENGTH", libraryParameters.get("NOMINAL_LENGTH")); - addOptionalAttribute(layoutElement, "NOMINAL_SDEV", libraryParameters.get("NOMINAL_SDEV")); + addOptionalAttribute( + layoutElement, + "NOMINAL_LENGTH", + resolveExperimentMetadata(libraryParameters, materialCharacteristics, "NOMINAL_LENGTH")); + addOptionalAttribute( + layoutElement, + "NOMINAL_SDEV", + resolveExperimentMetadata(libraryParameters, materialCharacteristics, "NOMINAL_SDEV")); } addOptionalTextElement( libraryDescriptorElement, "POOLING_STRATEGY", - libraryParameters.get("POOLING_STRATEGY")); + resolveExperimentMetadata(libraryParameters, materialCharacteristics, "POOLING_STRATEGY")); addOptionalTextElement( libraryDescriptorElement, "LIBRARY_CONSTRUCTION_PROTOCOL", - libraryParameters.get("LIBRARY_CONSTRUCTION_PROTOCOL")); + resolveExperimentMetadata( + libraryParameters, materialCharacteristics, "LIBRARY_CONSTRUCTION_PROTOCOL")); } /** Populates the ENA PLATFORM block directly from sequencing-process parameters. */ private void addPlatform( - final Element experimentElement, final Map sequencingParameters) { - final String platform = requireParameter(sequencingParameters, "PLATFORM"); - final String instrumentModel = requireParameter(sequencingParameters, "INSTRUMENT_MODEL"); + final Element experimentElement, + final Map sequencingParameters, + final Map materialCharacteristics) { + final String platform = + requireMetadataValue(sequencingParameters, materialCharacteristics, "PLATFORM"); + final String instrumentModel = + requireMetadataValue(sequencingParameters, materialCharacteristics, "INSTRUMENT_MODEL"); final Element platformElement = experimentElement.addElement("PLATFORM"); final Element platformTypeElement = platformElement.addElement(platform); @@ -522,10 +681,63 @@ private void addOptionalAttribute( } } - private String requireParameter( - final Map parameterValues, final String parameterName) { + private String requireMetadataValue( + final Map processMetadata, + final Map materialMetadata, + final String metadataName) { return requireValue( - parameterValues.get(parameterName), "parameter " + parameterName); + resolveExperimentMetadata(processMetadata, materialMetadata, metadataName), + "metadata " + metadataName); + } + + private String resolveExperimentMetadata( + final Map processMetadata, + final Map materialMetadata, + final String metadataName) { + return firstNonBlank( + getMetadataValue(processMetadata, metadataName), + getMetadataValue(materialMetadata, metadataName)); + } + + 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; + } + } + + 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(); + } + } + } + + 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 ""; + } + + return metadataKey.replaceAll("[^A-Za-z0-9]", "").toLowerCase(Locale.ROOT); } private String requireValue(final String value, final String fieldName) { 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 f16d2edf..aad01192 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 @@ -75,7 +75,10 @@ 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"); @@ -153,6 +156,56 @@ void testCreateENAExperimentSetElement() throws Exception { experimentSequence.forEach((key, value) -> System.out.println(key + " -> " + value)); } + @Test + 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 testCreateENAExperimentSetElementWithMultipleDataFiles() throws Exception { // This test verifies that multiple data files from the same library @@ -173,29 +226,12 @@ 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, + experimentSequence.size(), experiments.size(), - "Number of experiments should match number of unique libraries"); + "Number of experiments should match the resolved experiment sequence"); } } diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index ba98ce85..a9144b17 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.json @@ -797,30 +797,6 @@ "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": [ @@ -855,90 +831,40 @@ "@id": "#other_material/332", "name": "extract 1", "type": "library name", - "characteristics": [], - "derivesFrom": [ - { - "@id": "#sample/331" - } - ], - "comments": [] - }, - { - "@id": "#other_material/333", - "name": "library 1", - "type": "library name", "characteristics": [ { "category": { "@id": "#characteristic_category/Title_350" }, "value": { - "annotationValue": "library 1", - "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] - }, - { - "category": { - "@id": "#characteristic_category/submission_date_358" - }, - "value": { - "annotationValue": "", - "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] - }, - { - "category": { - "@id": "#characteristic_category/status_359" - }, - "value": { - "annotationValue": "", - "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] - }, - { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] + "termSource": "" + } } ], "derivesFrom": [ { - "@id": "#other_material/332" + "@id": "#sample/331" } ], "comments": [] }, { - "@id": "#other_material/337", - "name": "extract 2", + "@id": "#other_material/333", + "name": "library 1", "type": "library name", "characteristics": [], "derivesFrom": [ { - "@id": "#sample/431" + "@id": "#other_material/332" } ], "comments": [] }, { - "@id": "#other_material/338", - "name": "library 2", + "@id": "#other_material/337", + "name": "extract 2", "type": "library name", "characteristics": [ { @@ -946,50 +872,24 @@ "@id": "#characteristic_category/Title_350" }, "value": { - "annotationValue": "library 2", - "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] - }, - { - "category": { - "@id": "#characteristic_category/submission_date_358" - }, - "value": { - "annotationValue": "", - "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] - }, - { - "category": { - "@id": "#characteristic_category/status_359" - }, - "value": { - "annotationValue": "", + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment 2", "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] - }, + "termSource": "" + } + } + ], + "derivesFrom": [ { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", - "termAccession": "", - "termSource": "", - "comments": [] - }, - "comments": [] + "@id": "#sample/431" } ], + "comments": [] + }, + { + "@id": "#other_material/338", + "name": "library 2", + "type": "library name", + "characteristics": [], "derivesFrom": [ { "@id": "#other_material/337" diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index 63d9b89c..f61bf8d3 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -587,30 +587,6 @@ "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": [ @@ -642,7 +618,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" @@ -653,48 +640,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" diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index 1970fb9a..61e608d9 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-isa.json @@ -697,30 +697,6 @@ "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" } ], "materials": { @@ -733,79 +709,29 @@ { "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": "", + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", "termAccession": "", - "comments": [] + "termSource": "" } - }, - { - "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": [] - } - }, + } + ], + "derivesFrom": [ { - "category": { - "@id": "#characteristic_category/accession_360" - }, - "value": { - "annotationValue": "", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } + "@id": "#sample/331" } ], + "@id": "#other_material/332" + }, + { + "name": "library 1", + "type": "Extract Name", + "characteristics": [], "derivesFrom": [ { "@id": "#other_material/332" diff --git a/test-data/biosamples-original-isa-no-accesion-char.json b/test-data/biosamples-original-isa-no-accesion-char.json index 3587df33..cb57daa9 100644 --- a/test-data/biosamples-original-isa-no-accesion-char.json +++ b/test-data/biosamples-original-isa-no-accesion-char.json @@ -624,30 +624,6 @@ "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": [ @@ -667,79 +643,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" @@ -985,4 +911,4 @@ } ] } -} \ No newline at end of file +} diff --git a/test-data/biosamples-original-isa.json b/test-data/biosamples-original-isa.json index fc6bc10d..ff9ec7f4 100644 --- a/test-data/biosamples-original-isa.json +++ b/test-data/biosamples-original-isa.json @@ -663,30 +663,6 @@ "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": [ @@ -718,79 +694,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": "", - "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": "", + "annotationValue": "Arabidopsis leaf amplicon sequencing experiment", "termAccession": "", - "comments": [] + "termSource": "" } } ], + "derivesFrom": [ + { + "@id": "#sample/331" + } + ] + }, + { + "@id": "#other_material/333", + "name": "library 1", + "type": "Extract Name", + "characteristics": [], "derivesFrom": [ { "@id": "#other_material/332" @@ -979,28 +905,28 @@ "executesProtocol": { "@id": "#protocol/21_21" }, - "parameterValues": [ - { - "category": { - "@id": "#parameter/362" - }, - "value": { - "annotationValue": "OXFORD_NANOPORE", - "termSource": "", - "termAccession": "" - }, - "unit": { - "termSource": "", - "termAccession": "", - "comments": [] - } - }, - { - "category": { - "@id": "#parameter/363" + "parameterValues": [ + { + "category": { + "@id": "#parameter/362" + }, + "value": { + "annotationValue": "OXFORD_NANOPORE", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } }, - "value": { - "annotationValue": "MinION", + { + "category": { + "@id": "#parameter/363" + }, + "value": { + "annotationValue": "MinION", "termSource": "", "termAccession": "" }, From 5071194b4d78ba7c877b00eb05b50a1a1d5dd007 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 15:45:56 +0200 Subject: [PATCH 19/27] more clever experiment builder --- .../service/WebinExperimentXmlCreator.java | 177 +++++++++++------- .../WebinExperimentXmlCreatorTest.java | 58 ++++++ 2 files changed, 166 insertions(+), 69 deletions(-) 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 062b3855..b3b00294 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 @@ -182,6 +182,12 @@ private void mapExperimentForDataFile( findMaterialLineageToSample(library, assay.getMaterials()); final ProcessSequence libraryConstructionProcess = findProcessByOutputId(assay.getProcessSequence(), library.getId()); + final List experimentProcesses = + findExperimentProcesses( + assay.getProcessSequence(), + libraryConstructionProcess, + sequencingProcess, + materialLineage); createExperimentElement( root, @@ -189,8 +195,7 @@ private void mapExperimentForDataFile( library, materialLineage, assay, - libraryConstructionProcess, - sequencingProcess, + experimentProcesses, protocolToParameterNameMap, bioSampleAccessions, experimentId, @@ -253,6 +258,44 @@ private OtherMaterial findLibraryFromProcessInput( return null; } + 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, findProcessByOutputId(assayProcesses, material.getId())); + } + + return experimentProcesses; + } + + private void addExperimentProcess( + final List experimentProcesses, + final Set processIds, + final ProcessSequence process) { + if (process == null || process.getId() == null || !processIds.add(process.getId())) { + return; + } + + experimentProcesses.add(process); + } + private List findMaterialLineageToSample( final OtherMaterial material, final Materials materials) { final List materialLineage = new ArrayList<>(); @@ -313,19 +356,16 @@ private void createExperimentElement( final OtherMaterial library, final List materialLineage, final Assay assay, - final ProcessSequence libraryConstructionProcess, - final ProcessSequence sequencingProcess, + final List experimentProcesses, final Map> protocolToParameterNameMap, final Map bioSampleAccessions, final String experimentId, final String randomSubmissionIdentifier) { - final Map libraryParameters = - extractParameterValues(libraryConstructionProcess, protocolToParameterNameMap); - final Map sequencingParameters = - extractParameterValues(sequencingProcess, protocolToParameterNameMap); - final Map materialCharacteristics = - extractMaterialCharacteristicValues( - materialLineage, buildCharacteristicKeyLookup(study, assay)); + final ExperimentMetadata experimentMetadata = + new ExperimentMetadata( + extractParameterValues(experimentProcesses, protocolToParameterNameMap), + extractMaterialCharacteristicValues( + materialLineage, buildCharacteristicKeyLookup(study, assay))); final Element experimentElement = root.addElement("EXPERIMENT"); experimentElement.addAttribute("alias", experimentId); @@ -333,11 +373,7 @@ private void createExperimentElement( .addElement("TITLE") .addText( firstNonBlank( - getMetadataValue(libraryParameters, "EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), - getMetadataValue( - sequencingParameters, "EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), - getMetadataValue( - materialCharacteristics, "EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), + experimentMetadata.get("EXPERIMENT_TITLE", "EXPERIMENT TITLE", "TITLE"), library.getName())); experimentElement .addElement("STUDY_REF") @@ -346,9 +382,7 @@ private void createExperimentElement( final Element designElement = experimentElement.addElement("DESIGN"); designElement .addElement("DESIGN_DESCRIPTION") - .addText( - requireMetadataValue( - libraryParameters, materialCharacteristics, "DESIGN_DESCRIPTION")); + .addText(experimentMetadata.require("DESIGN_DESCRIPTION")); final String sampleAccession = resolveSampleAccessionForLibrary(study, assay, library); designElement @@ -356,9 +390,8 @@ private void createExperimentElement( .addAttribute("accession", requireValue(sampleAccession, "BioSamples sample accession")); final Element libraryDescriptorElement = designElement.addElement("LIBRARY_DESCRIPTOR"); - addLibraryDescriptor( - libraryDescriptorElement, library, libraryParameters, materialCharacteristics); - addPlatform(experimentElement, sequencingParameters, materialCharacteristics); + addLibraryDescriptor(libraryDescriptorElement, library, experimentMetadata); + addPlatform(experimentElement, experimentMetadata); } /** @@ -560,6 +593,23 @@ private String characteristicCategoryIdToName(final String characteristicCategor * 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 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) { @@ -584,7 +634,8 @@ private Map extractParameterValues( || parameterValue.getCategory() == null || parameterValue.getCategory().getId() == null || parameterValue.getValue() == null - || parameterValue.getValue().getAnnotationValue() == null) { + || parameterValue.getValue().getAnnotationValue() == null + || parameterValue.getValue().getAnnotationValue().isBlank()) { continue; } @@ -603,64 +654,49 @@ private Map extractParameterValues( private void addLibraryDescriptor( final Element libraryDescriptorElement, final OtherMaterial library, - final Map libraryParameters, - final Map materialCharacteristics) { + final ExperimentMetadata experimentMetadata) { addOptionalTextElement( libraryDescriptorElement, "LIBRARY_NAME", firstNonBlank( - getMetadataValue(libraryParameters, "LIBRARY_NAME", "LIBRARY NAME"), - getMetadataValue(materialCharacteristics, "LIBRARY_NAME", "LIBRARY NAME"), + experimentMetadata.get("LIBRARY_NAME", "LIBRARY NAME"), library.getName())); libraryDescriptorElement .addElement("LIBRARY_STRATEGY") - .addText( - requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_STRATEGY")); + .addText(experimentMetadata.require("LIBRARY_STRATEGY")); libraryDescriptorElement .addElement("LIBRARY_SOURCE") - .addText( - requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_SOURCE")); + .addText(experimentMetadata.require("LIBRARY_SOURCE")); libraryDescriptorElement .addElement("LIBRARY_SELECTION") - .addText( - requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_SELECTION")); + .addText(experimentMetadata.require("LIBRARY_SELECTION")); final Element libraryLayoutElement = libraryDescriptorElement.addElement("LIBRARY_LAYOUT"); - final String layout = - requireMetadataValue(libraryParameters, materialCharacteristics, "LIBRARY_LAYOUT"); + final String layout = experimentMetadata.require("LIBRARY_LAYOUT"); final Element layoutElement = libraryLayoutElement.addElement(layout); if ("PAIRED".equals(layout)) { addOptionalAttribute( - layoutElement, - "NOMINAL_LENGTH", - resolveExperimentMetadata(libraryParameters, materialCharacteristics, "NOMINAL_LENGTH")); + layoutElement, "NOMINAL_LENGTH", experimentMetadata.get("NOMINAL_LENGTH")); addOptionalAttribute( - layoutElement, - "NOMINAL_SDEV", - resolveExperimentMetadata(libraryParameters, materialCharacteristics, "NOMINAL_SDEV")); + layoutElement, "NOMINAL_SDEV", experimentMetadata.get("NOMINAL_SDEV")); } addOptionalTextElement( libraryDescriptorElement, "POOLING_STRATEGY", - resolveExperimentMetadata(libraryParameters, materialCharacteristics, "POOLING_STRATEGY")); + experimentMetadata.get("POOLING_STRATEGY")); addOptionalTextElement( libraryDescriptorElement, "LIBRARY_CONSTRUCTION_PROTOCOL", - resolveExperimentMetadata( - libraryParameters, materialCharacteristics, "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 Map sequencingParameters, - final Map materialCharacteristics) { - final String platform = - requireMetadataValue(sequencingParameters, materialCharacteristics, "PLATFORM"); - final String instrumentModel = - requireMetadataValue(sequencingParameters, materialCharacteristics, "INSTRUMENT_MODEL"); + 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); @@ -681,24 +717,6 @@ private void addOptionalAttribute( } } - private String requireMetadataValue( - final Map processMetadata, - final Map materialMetadata, - final String metadataName) { - return requireValue( - resolveExperimentMetadata(processMetadata, materialMetadata, metadataName), - "metadata " + metadataName); - } - - private String resolveExperimentMetadata( - final Map processMetadata, - final Map materialMetadata, - final String metadataName) { - return firstNonBlank( - getMetadataValue(processMetadata, metadataName), - getMetadataValue(materialMetadata, metadataName)); - } - private String getMetadataValue(final Map metadata, final String... metadataNames) { if (metadata == null || metadataNames == null) { return null; @@ -740,6 +758,27 @@ private String normalizeMetadataKey(final String metadataKey) { 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; + } + + private String get(final String... metadataNames) { + return firstNonBlank( + getMetadataValue(processMetadata, metadataNames), + getMetadataValue(materialMetadata, metadataNames)); + } + + private String require(final String metadataName) { + return requireValue(get(metadataName), "metadata " + metadataName); + } + } + private String requireValue(final String value, final String fieldName) { if (value == null || value.isBlank()) { throw new IllegalArgumentException( 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 aad01192..808e3f95 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 @@ -206,6 +206,64 @@ void testExperimentTitleCanComeFromProcessMetadata() throws Exception { "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 { // This test verifies that multiple data files from the same library From b93dd42f4851e30630d0ceaaf9fc58a4d2380fee Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 16:09:01 +0200 Subject: [PATCH 20/27] more clever sample submission including parameter value submissions --- .../service/BioSamplesSubmitter.java | 201 ++++++++++++++++-- .../ISAToSRA/BiosampleReceiptToMarsTest.java | 48 +++++ test-data/biosamples-input-isa-multi.json | 37 +++- test-data/biosamples-input-isa.json | 24 ++- test-data/biosamples-modified-isa.json | 24 ++- ...samples-original-isa-no-accesion-char.json | 24 ++- test-data/biosamples-original-isa.json | 24 ++- 7 files changed, 349 insertions(+), 33 deletions(-) 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..b0456b58 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 @@ -39,6 +39,8 @@ public BiosampleAccessionsMap createBioSamples( study -> { final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study); + final Map> protocolToParameterNameMap = + buildProtocolToParameterNameLookup(study); typeToBioSamplesAccessionMap.studyAccessionsMap = new ReceiptAccessionsMap(Study.Fields.title, study.getTitle()); @@ -49,7 +51,12 @@ public BiosampleAccessionsMap createBioSamples( sample -> { final BioSample persistedChildSample = this.createAndUpdateChildSampleWithRelationship( - sample, sourceBioSample, webinToken, characteristicKeyLookup); + sample, + sourceBioSample, + webinToken, + characteristicKeyLookup, + protocolToParameterNameMap, + findProcessByOutputId(study.getProcessSequence(), sample.getId())); if (persistedChildSample != null) { typeToBioSamplesAccessionMap.sampleAccessionsMap.isaItemName = @@ -70,9 +77,15 @@ 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); + addAttributesIfMissing( + childSampleAttributes, + buildAttributesFromProcessParameters( + sampleCollectionProcess, protocolToParameterNameMap)); synchronizeSharedAttribute(childSampleAttributes, sourceBioSample, "organism"); synchronizeSharedAttribute(childSampleAttributes, sourceBioSample, "tax_id"); copySourceAttributeIfMissing(childSampleAttributes, sourceBioSample, "collection date"); @@ -119,22 +132,9 @@ private List createSourceBioSample( 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 SortedSet attributes = + buildAttributesFromCharacteristics( + source.getCharacteristics(), characteristicKeyLookup); final BioSample sourceSample = new BioSample.Builder(source.getName()) .withRelease(Instant.now()) @@ -146,6 +146,27 @@ private List createSourceBioSample( return biosamples; } + 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( final List characteristics, final Map characteristicKeyLookup) { final SortedSet attributes = new TreeSet<>(); @@ -174,6 +195,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,6 +261,74 @@ 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, @@ -264,14 +394,41 @@ private static Characteristic getBioSampleAccessionCharacteristic( */ 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 +450,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..53764382 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,49 @@ 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(); + + Assertions.assertTrue( + leafSample.getAttributes().stream() + .anyMatch( + attribute -> + "isolation_source".equals(attribute.getType()) + && "seedling leaf tissue".equals(attribute.getValue())), + "Expected child BioSample to include the sample-collection process parameter."); + } + + private static class CapturingBioSamplesSubmitter extends BioSamplesSubmitter { + private final List updatedSamples = new ArrayList<>(); + private int accessionSequence = 1; + + @Override + protected BioSample createSampleInBioSamples(final BioSample sample, final String webinToken) { + return BioSample.Builder.fromSample(sample) + .withAccession("SAMEA_TEST_" + accessionSequence++) + .build(); + } + + @Override + protected BioSample updateSampleWithRelationshipsToBioSamples( + final BioSample sampleWithRelationship, final String webinToken) { + updatedSamples.add(sampleWithRelationship); + return sampleWithRelationship; + } + } } diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index a9144b17..9022ba0a 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.json @@ -528,7 +528,16 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -718,7 +727,18 @@ "derivesFrom": [] } ], - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "seedling leaf tissue", + "termSource": "", + "termAccession": "" + } + } + ], "nextProcess": { "inputs": [], "outputs": [], @@ -758,7 +778,18 @@ "derivesFrom": [] } ], - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "seedling root tissue", + "termSource": "", + "termAccession": "" + } + } + ], "nextProcess": { "inputs": [], "outputs": [], diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index f61bf8d3..a10a8455 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -376,7 +376,16 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -547,7 +556,18 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "seedling leaf tissue", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index 61e608d9..4cecaf0a 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-isa.json @@ -468,7 +468,16 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -639,7 +648,18 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "seedling leaf tissue", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, diff --git a/test-data/biosamples-original-isa-no-accesion-char.json b/test-data/biosamples-original-isa-no-accesion-char.json index cb57daa9..323fe6cb 100644 --- a/test-data/biosamples-original-isa-no-accesion-char.json +++ b/test-data/biosamples-original-isa-no-accesion-char.json @@ -429,7 +429,16 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -584,7 +593,18 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "seedling leaf tissue", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, diff --git a/test-data/biosamples-original-isa.json b/test-data/biosamples-original-isa.json index ff9ec7f4..8157f15d 100644 --- a/test-data/biosamples-original-isa.json +++ b/test-data/biosamples-original-isa.json @@ -452,7 +452,16 @@ "description": "", "uri": "", "version": "", - "parameters": [], + "parameters": [ + { + "@id": "#parameter/sample_collection_isolation_source", + "parameterName": { + "annotationValue": "isolation_source", + "termAccession": "", + "termSource": "" + } + } + ], "components": [ { "componentName": "", @@ -623,7 +632,18 @@ "executesProtocol": { "@id": "#protocol/18_10" }, - "parameterValues": [], + "parameterValues": [ + { + "category": { + "@id": "#parameter/sample_collection_isolation_source" + }, + "value": { + "annotationValue": "seedling leaf tissue", + "termSource": "", + "termAccession": "" + } + } + ], "performer": "", "date": "", "previousProcess": {}, From 55ee59d67d0171223c4049db71119c27f383fcfa Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 16:21:04 +0200 Subject: [PATCH 21/27] fix relationship error --- .../service/BioSamplesSubmitter.java | 88 ++++++++++++++++--- 1 file changed, 74 insertions(+), 14 deletions(-) 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 b0456b58..1a7fd862 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 @@ -28,12 +28,16 @@ public BiosampleAccessionsMap createBioSamples( 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); + final Map sourceBioSamplesById = + this.createSourceBioSamplesById(studies, webinToken); typeToBioSamplesAccessionMap.sourceAccessionsMap.isaItemName = Source.Fields.name; - typeToBioSamplesAccessionMap.sourceAccessionsMap.accessionMap.put( - sourceBioSample.getName(), sourceBioSample.getAccession()); + sourceBioSamplesById + .values() + .forEach( + sourceBioSample -> + typeToBioSamplesAccessionMap.sourceAccessionsMap.accessionMap.put( + sourceBioSample.getName(), sourceBioSample.getAccession())); studies.forEach( study -> { @@ -49,6 +53,11 @@ public BiosampleAccessionsMap createBioSamples( .getSamples() .forEach( sample -> { + final ProcessSequence sampleCollectionProcess = + findProcessByOutputId(study.getProcessSequence(), sample.getId()); + final BioSample sourceBioSample = + findSourceBioSampleForSample( + sample, sampleCollectionProcess, sourceBioSamplesById); final BioSample persistedChildSample = this.createAndUpdateChildSampleWithRelationship( sample, @@ -56,7 +65,7 @@ public BiosampleAccessionsMap createBioSamples( webinToken, characteristicKeyLookup, protocolToParameterNameMap, - findProcessByOutputId(study.getProcessSequence(), sample.getId())); + sampleCollectionProcess); if (persistedChildSample != null) { typeToBioSamplesAccessionMap.sampleAccessionsMap.isaItemName = @@ -119,9 +128,9 @@ private BioSample createAndUpdateChildSampleWithRelationship( } } - private List createSourceBioSample( + private Map createSourceBioSamplesById( final List studies, final String webinToken) { - List biosamples = new ArrayList<>(); + final Map biosamplesBySourceId = new LinkedHashMap<>(); studies.forEach( study -> @@ -135,15 +144,66 @@ private List createSourceBioSample( final SortedSet attributes = buildAttributesFromCharacteristics( source.getCharacteristics(), characteristicKeyLookup); - final BioSample sourceSample = - new BioSample.Builder(source.getName()) - .withRelease(Instant.now()) - .withAttributes(attributes) - .build(); - biosamples.add(this.createSampleInBioSamples(sourceSample, webinToken)); + final BioSample sourceSample = + new BioSample.Builder(source.getName()) + .withRelease(Instant.now()) + .withAttributes(attributes) + .build(); + biosamplesBySourceId.put( + source.getId(), this.createSampleInBioSamples(sourceSample, webinToken)); })); - return biosamples; + return biosamplesBySourceId; + } + + private BioSample findSourceBioSampleForSample( + final Sample sample, + final ProcessSequence sampleCollectionProcess, + final Map sourceBioSamplesById) { + final Optional sampleSourceId = + findSourceIdFromSampleDerivesFrom(sample, sourceBioSamplesById); + if (sampleSourceId.isPresent()) { + return sourceBioSamplesById.get(sampleSourceId.get()); + } + + final Optional processSourceId = + findSourceIdFromProcessInputs(sampleCollectionProcess, sourceBioSamplesById); + if (processSourceId.isPresent()) { + return sourceBioSamplesById.get(processSourceId.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( From f20687fa3f6c86cfa530b00f016f9a88514aacbd Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 16:30:43 +0200 Subject: [PATCH 22/27] add extra tests --- .../ISAToSRA/BiosampleReceiptToMarsTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) 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 53764382..1ea56e86 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 @@ -84,6 +84,42 @@ void createBioSamplesUsesSampleCollectionProcessParametersForChildSampleAttribut "Expected child BioSample to include the sample-collection process parameter."); } + @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, "isolation_source", "seedling root tissue")); + } + + 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 updatedSamples = new ArrayList<>(); private int accessionSequence = 1; @@ -101,5 +137,12 @@ protected BioSample updateSampleWithRelationshipsToBioSamples( updatedSamples.add(sampleWithRelationship); return sampleWithRelationship; } + + private BioSample updatedSampleByName(final String sampleName) { + return updatedSamples.stream() + .filter(sample -> sampleName.equals(sample.getName())) + .findFirst() + .orElseThrow(); + } } } From a65f10a2f9b5ebccf0ca8dbaaf463ae9a2e10666 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 16:43:51 +0200 Subject: [PATCH 23/27] add richer examples --- .../ISAToSRA/BiosampleReceiptToMarsTest.java | 40 +++- test-data/biosamples-input-isa-multi.json | 182 ++++++++++++++++-- test-data/biosamples-input-isa.json | 119 ++++++++++-- test-data/biosamples-modified-isa.json | 121 ++++++++++-- ...samples-original-isa-no-accesion-char.json | 126 ++++++++++-- test-data/biosamples-original-isa.json | 124 ++++++++++-- 6 files changed, 631 insertions(+), 81 deletions(-) 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 1ea56e86..cf80afc4 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 @@ -74,14 +74,29 @@ void createBioSamplesUsesSampleCollectionProcessParametersForChildSampleAttribut .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()) - && "seedling leaf tissue".equals(attribute.getValue())), + && "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 @@ -100,7 +115,11 @@ void createBioSamplesKeepsEachChildSampleLinkedToItsOwnSource() throws Exception 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, "isolation_source", "seedling root tissue")); + 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")); } private static String derivedFromTarget(final BioSample sample) { @@ -121,14 +140,18 @@ private static boolean hasAttribute( } 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) { - return BioSample.Builder.fromSample(sample) - .withAccession("SAMEA_TEST_" + accessionSequence++) - .build(); + final BioSample createdSample = + BioSample.Builder.fromSample(sample) + .withAccession("SAMEA_TEST_" + accessionSequence++) + .build(); + createdSamples.add(createdSample); + return createdSample; } @Override @@ -144,5 +167,12 @@ private BioSample updatedSampleByName(final String sampleName) { .findFirst() .orElseThrow(); } + + private BioSample createdSampleByName(final String sampleName) { + return createdSamples.stream() + .filter(sample -> sampleName.equals(sample.getName())) + .findFirst() + .orElseThrow(); + } } } diff --git a/test-data/biosamples-input-isa-multi.json b/test-data/biosamples-input-isa-multi.json index 9022ba0a..0f8c304c 100644 --- a/test-data/biosamples-input-isa-multi.json +++ b/test-data/biosamples-input-isa-multi.json @@ -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": "Col-0", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" }, "value": { - "annotationValue": "na", + "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": "" } @@ -395,10 +429,20 @@ }, { "category": { - "@id": "#characteristic_category/cell_type_321" + "@id": "#characteristic_category/genotype_330" + }, + "value": { + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" }, "value": { - "annotationValue": "root tissue", + "annotationValue": "greenhouse soil bench", "termSource": "", "termAccession": "" } @@ -408,7 +452,7 @@ "@id": "#characteristic_category/dev_stage_322" }, "value": { - "annotationValue": "flowering", + "annotationValue": "flowering stage", "termSource": "", "termAccession": "" } @@ -428,7 +472,7 @@ "@id": "#characteristic_category/isolation_source_324" }, "value": { - "annotationValue": "root", + "annotationValue": "whole plant", "termSource": "", "termAccession": "" } @@ -500,7 +544,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": [] }, { @@ -511,7 +576,28 @@ "@id": "#source/430" } ], - "characteristics": [], + "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": [] } ] @@ -536,6 +622,22 @@ "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": [ @@ -733,7 +835,27 @@ "@id": "#parameter/sample_collection_isolation_source" }, "value": { - "annotationValue": "seedling leaf tissue", + "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": "" } @@ -784,7 +906,27 @@ "@id": "#parameter/sample_collection_isolation_source" }, "value": { - "annotationValue": "seedling root tissue", + "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": "" } diff --git a/test-data/biosamples-input-isa.json b/test-data/biosamples-input-isa.json index a10a8455..eeb2919c 100644 --- a/test-data/biosamples-input-isa.json +++ b/test-data/biosamples-input-isa.json @@ -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": [] } ] @@ -384,6 +439,22 @@ "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": [ @@ -562,7 +633,27 @@ "@id": "#parameter/sample_collection_isolation_source" }, "value": { - "annotationValue": "seedling leaf tissue", + "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": "" } diff --git a/test-data/biosamples-modified-isa.json b/test-data/biosamples-modified-isa.json index 4cecaf0a..c4bdd180 100644 --- a/test-data/biosamples-modified-isa.json +++ b/test-data/biosamples-modified-isa.json @@ -133,14 +133,6 @@ }, "@id": "#characteristic_category/organism_320" }, - { - "characteristicType": { - "annotationValue": "cell_type", - "termAccession": "", - "termSource": "" - }, - "@id": "#characteristic_category/cell_type_321" - }, { "characteristicType": { "annotationValue": "dev_stage", @@ -204,6 +196,38 @@ "termSource": "" }, "@id": "#characteristic_category/accession_329" + }, + { + "characteristicType": { + "annotationValue": "genotype", + "termAccession": "", + "termSource": "" + }, + "@id": "#characteristic_category/genotype_330" + }, + { + "characteristicType": { + "annotationValue": "growth condition", + "termAccession": "", + "termSource": "" + }, + "@id": "#characteristic_category/growth_condition_331" + }, + { + "characteristicType": { + "annotationValue": "organism part", + "termAccession": "", + "termSource": "" + }, + "@id": "#characteristic_category/organism_part_332" + }, + { + "characteristicType": { + "annotationValue": "sample description", + "termAccession": "", + "termSource": "" + }, + "@id": "#characteristic_category/sample_description_333" } ], "materials": { @@ -273,10 +297,25 @@ }, { "category": { - "@id": "#characteristic_category/cell_type_321" + "@id": "#characteristic_category/genotype_330" + }, + "value": { + "annotationValue": "Col-0", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" }, "value": { - "annotationValue": "na", + "annotationValue": "16 h light / 8 h dark growth chamber", "termSource": "", "termAccession": "" }, @@ -291,7 +330,7 @@ "@id": "#characteristic_category/dev_stage_322" }, "value": { - "annotationValue": "budding", + "annotationValue": "vegetative rosette stage", "termSource": "", "termAccession": "" }, @@ -321,7 +360,7 @@ "@id": "#characteristic_category/isolation_source_324" }, "value": { - "annotationValue": "seed", + "annotationValue": "whole plant", "termSource": "", "termAccession": "" }, @@ -427,6 +466,26 @@ } ], "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" @@ -476,6 +535,22 @@ "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": [ @@ -654,7 +729,27 @@ "@id": "#parameter/sample_collection_isolation_source" }, "value": { - "annotationValue": "seedling leaf tissue", + "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": "" } diff --git a/test-data/biosamples-original-isa-no-accesion-char.json b/test-data/biosamples-original-isa-no-accesion-char.json index 323fe6cb..6c1747df 100644 --- a/test-data/biosamples-original-isa-no-accesion-char.json +++ b/test-data/biosamples-original-isa-no-accesion-char.json @@ -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": "Col-0", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" }, "value": { - "annotationValue": "na", + "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": { @@ -437,6 +497,22 @@ "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": [ @@ -599,7 +675,27 @@ "@id": "#parameter/sample_collection_isolation_source" }, "value": { - "annotationValue": "seedling leaf tissue", + "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": "" } diff --git a/test-data/biosamples-original-isa.json b/test-data/biosamples-original-isa.json index 8157f15d..930459a6 100644 --- a/test-data/biosamples-original-isa.json +++ b/test-data/biosamples-original-isa.json @@ -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": "Col-0", + "termSource": "", + "termAccession": "" + }, + "unit": { + "termSource": "", + "termAccession": "", + "comments": [] + } + }, + { + "category": { + "@id": "#characteristic_category/growth_condition_331" }, "value": { - "annotationValue": "na", + "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": "" }, @@ -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": { @@ -460,6 +520,22 @@ "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": [ @@ -638,7 +714,27 @@ "@id": "#parameter/sample_collection_isolation_source" }, "value": { - "annotationValue": "seedling leaf tissue", + "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": "" } From 002d734ae0f9017f94cb7f41499b621781de843c Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 16:59:25 +0200 Subject: [PATCH 24/27] add multi sample test --- .../ISAToSRA/BiosampleReceiptToMarsTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) 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 cf80afc4..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 @@ -122,6 +122,41 @@ void createBioSamplesKeepsEachChildSampleLinkedToItsOwnSource() throws Exception 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())) From 3efcae119a806a3fc5901afdf6dd1af78fb2c205 Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Fri, 10 Jul 2026 17:20:21 +0200 Subject: [PATCH 25/27] Refactor BioSamples Submitter --- .../service/BioSamplesSubmitter.java | 213 +++++++++--------- 1 file changed, 104 insertions(+), 109 deletions(-) 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 1a7fd862..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,58 +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 { - final Map sourceBioSamplesById = - this.createSourceBioSamplesById(studies, webinToken); - typeToBioSamplesAccessionMap.sourceAccessionsMap.isaItemName = Source.Fields.name; - sourceBioSamplesById - .values() - .forEach( - sourceBioSample -> - typeToBioSamplesAccessionMap.sourceAccessionsMap.accessionMap.put( - sourceBioSample.getName(), sourceBioSample.getAccession())); - - studies.forEach( - study -> { - final Map characteristicKeyLookup = - buildCharacteristicKeyLookup(study); - final Map> protocolToParameterNameMap = - buildProtocolToParameterNameLookup(study); - typeToBioSamplesAccessionMap.studyAccessionsMap = - new ReceiptAccessionsMap(Study.Fields.title, study.getTitle()); - - study - .getMaterials() - .getSamples() - .forEach( - sample -> { - 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.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); } @@ -90,16 +83,12 @@ private BioSample createAndUpdateChildSampleWithRelationship( final Map> protocolToParameterNameMap, final ProcessSequence sampleCollectionProcess) { final SortedSet childSampleAttributes = - buildAttributesFromCharacteristics(sample.getCharacteristics(), characteristicKeyLookup); - addAttributesIfMissing( - childSampleAttributes, - buildAttributesFromProcessParameters( - sampleCollectionProcess, protocolToParameterNameMap)); - 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()) @@ -128,48 +117,73 @@ private BioSample createAndUpdateChildSampleWithRelationship( } } + 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 List studies, final String webinToken) { + final Study study, + final Map characteristicKeyLookup, + final String webinToken) { final Map biosamplesBySourceId = new LinkedHashMap<>(); - studies.forEach( - study -> - study - .getMaterials() - .getSources() - .forEach( - source -> { - final Map characteristicKeyLookup = - buildCharacteristicKeyLookup(study); - 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)); - })); + 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) { - final Optional sampleSourceId = - findSourceIdFromSampleDerivesFrom(sample, sourceBioSamplesById); - if (sampleSourceId.isPresent()) { - return sourceBioSamplesById.get(sampleSourceId.get()); - } - - final Optional processSourceId = - findSourceIdFromProcessInputs(sampleCollectionProcess, sourceBioSamplesById); - if (processSourceId.isPresent()) { - return sourceBioSamplesById.get(processSourceId.get()); + // 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) { @@ -393,18 +407,14 @@ 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( @@ -433,21 +443,6 @@ 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. From aaa5a24552485c772b15a0998b145c68d4a5b82b Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Mon, 13 Jul 2026 09:56:08 +0200 Subject: [PATCH 26/27] Refactor code --- .../sra/service/IsaJsonGraphLookup.java | 140 ++++++++++++++++++ .../sra/service/MarsReceiptService.java | 51 +------ .../sra/service/SRAAnalysisXmlCreator.java | 65 ++++---- .../service/WebinExperimentXmlCreator.java | 133 ++++++----------- .../sra/service/WebinRunXmlCreator.java | 88 +---------- 5 files changed, 226 insertions(+), 251 deletions(-) create mode 100644 repository-services/isajson-ena/src/main/java/com/elixir/biohackaton/ISAToSRA/sra/service/IsaJsonGraphLookup.java 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 c3b245cf..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 @@ -8,7 +8,6 @@ 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.Output; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ProcessSequence; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsError; import com.elixir.biohackaton.ISAToSRA.receipt.marsmodel.MarsErrorType; @@ -20,11 +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.HashSet; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -171,12 +170,7 @@ private List getDataFileIdsForSequencingProcess( .findFirst() .ifPresent( process -> - Optional.ofNullable(process.getOutputs()) - .orElse(new ArrayList<>()) - .stream() - .map(Output::getId) - .filter(id -> id != null && !id.isBlank()) - .map(this::normalizeDataFileId) + IsaJsonGraphLookup.normalizedOutputIds(process) .forEach(dataFileIds::add)))); return dataFileIds; @@ -209,54 +203,17 @@ private void addAssaySequencingProcessOutputs( for (final DataFile dataFile : assay.getDataFiles()) { final ProcessSequence sequencingProcess = - findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); + IsaJsonGraphLookup.findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); if (sequencingProcess == null || !processedSequencingProcesses.add(sequencingProcess.getId())) { continue; } final List dataFileIds = new ArrayList<>(); - Optional.ofNullable(sequencingProcess.getOutputs()) - .orElse(new ArrayList<>()) - .stream() - .map(Output::getId) - .filter(id -> id != null && !id.isBlank()) - .map(this::normalizeDataFileId) - .forEach(dataFileIds::add); + IsaJsonGraphLookup.normalizedOutputIds(sequencingProcess).forEach(dataFileIds::add); sequencingProcessDataFiles.add(dataFileIds); } } - private 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.getOutputs() == null) { - continue; - } - - for (final Output output : process.getOutputs()) { - if (output.getId() == null) { - continue; - } - - if (normalizeDataFileId(output.getId()).equals(normalizedOutputId)) { - return process; - } - } - } - - return null; - } - - private String normalizeDataFileId(final String id) { - return id == null ? null : id.replace("#data_file/", "#data/"); - } - private boolean aliasAccessionPairFilter(ReceiptObject item) { if (item == null) { setMarsReceiptErrors("ENA receipt: Item is NULL"); 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 b3b00294..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 @@ -8,10 +8,8 @@ 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.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.Parameter; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ParameterValue; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.ProcessSequence; @@ -31,8 +29,6 @@ @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. * @@ -162,14 +158,15 @@ private void mapExperimentForDataFile( final Assay assay, final DataFile dataFile) { final ProcessSequence sequencingProcess = - findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); + IsaJsonGraphLookup.findProcessByOutputId(assay.getProcessSequence(), dataFile.getId()); if (sequencingProcess == null) { return; } final OtherMaterial library = - findLibraryFromProcessInput(sequencingProcess, assay.getMaterials()); + IsaJsonGraphLookup.findOtherMaterialFromProcessInput( + sequencingProcess, assay.getMaterials()); if (library == null || experimentSequence.containsKey(library.getId())) { return; @@ -181,7 +178,7 @@ private void mapExperimentForDataFile( final List materialLineage = findMaterialLineageToSample(library, assay.getMaterials()); final ProcessSequence libraryConstructionProcess = - findProcessByOutputId(assay.getProcessSequence(), library.getId()); + IsaJsonGraphLookup.findProcessByOutputId(assay.getProcessSequence(), library.getId()); final List experimentProcesses = findExperimentProcesses( assay.getProcessSequence(), @@ -202,62 +199,6 @@ private void mapExperimentForDataFile( randomSubmissionIdentifier); } - private 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.getOutputs() == null) { - continue; - } - - for (final Output output : process.getOutputs()) { - if (output.getId() == null) { - continue; - } - - final String normalizedProcessOutputId = normalizeDataFileId(output.getId()); - if (normalizedProcessOutputId.equals(normalizedOutputId)) { - return process; - } - } - } - - return null; - } - - private String normalizeDataFileId(final String id) { - if (id == null) { - return null; - } - return id.replace("#data_file/", "#data/"); - } - - 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) { - continue; - } - - for (final OtherMaterial otherMaterial : materials.getOtherMaterials()) { - if (otherMaterial.getId() != null && otherMaterial.getId().equals(input.getId())) { - return otherMaterial; - } - } - } - - return null; - } - private List findExperimentProcesses( final List assayProcesses, final ProcessSequence libraryConstructionProcess, @@ -279,7 +220,9 @@ private List findExperimentProcesses( } addExperimentProcess( - experimentProcesses, processIds, findProcessByOutputId(assayProcesses, material.getId())); + experimentProcesses, + processIds, + IsaJsonGraphLookup.findProcessByOutputId(assayProcesses, material.getId())); } return experimentProcesses; @@ -296,31 +239,20 @@ private void addExperimentProcess( experimentProcesses.add(process); } + /** + * 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 = buildOtherMaterialsById(materials); + final Map otherMaterialsById = + IsaJsonGraphLookup.buildOtherMaterialsById(materials); collectMaterialLineage(material, otherMaterialsById, materialLineage, new HashSet<>()); return materialLineage; } - private 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; - } - private void collectMaterialLineage( final OtherMaterial material, final Map otherMaterialsById, @@ -361,6 +293,7 @@ private void createExperimentElement( final Map bioSampleAccessions, final String experimentId, final String randomSubmissionIdentifier) { + // Process parameters are closest to ENA submission fields; material characteristics fill gaps. final ExperimentMetadata experimentMetadata = new ExperimentMetadata( extractParameterValues(experimentProcesses, protocolToParameterNameMap), @@ -384,7 +317,8 @@ private void createExperimentElement( .addElement("DESIGN_DESCRIPTION") .addText(experimentMetadata.require("DESIGN_DESCRIPTION")); - final String sampleAccession = resolveSampleAccessionForLibrary(study, assay, library); + final String sampleAccession = + resolveSampleAccessionForLibrary(study, assay, library, bioSampleAccessions); designElement .addElement("SAMPLE_DESCRIPTOR") .addAttribute("accession", requireValue(sampleAccession, "BioSamples sample accession")); @@ -400,9 +334,10 @@ private void createExperimentElement( private String resolveSampleAccessionForLibrary( final Study study, final Assay assay, - final OtherMaterial library) { + final OtherMaterial library, + final Map bioSampleAccessions) { if (study == null || study.getMaterials() == null || study.getMaterials().getSamples() == null) { - return ""; + return getBioSampleAccessionFallback(bioSampleAccessions, null); } final Map samplesById = new HashMap<>(); @@ -413,16 +348,42 @@ private String resolveSampleAccessionForLibrary( } final Map otherMaterialsById = - buildOtherMaterialsById(assay.getMaterials()); + IsaJsonGraphLookup.buildOtherMaterialsById(assay.getMaterials()); final Sample sample = findSampleForMaterialId(library.getId(), samplesById, otherMaterialsById, new HashSet<>()); if (sample == null) { - return ""; + return getBioSampleAccessionFallback(bioSampleAccessions, null); } final Map characteristicKeyLookup = buildCharacteristicKeyLookup(study, assay); - return getCharacteristicAnnotation(sample.getCharacteristics(), characteristicKeyLookup); + 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( 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 72d16a45..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,7 +2,6 @@ package com.elixir.biohackaton.ISAToSRA.sra.service; import com.elixir.biohackaton.ISAToSRA.receipt.isamodel.*; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -34,7 +33,7 @@ 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) { @@ -45,7 +44,7 @@ public void createENARunSetElement( // Find the library (experiment) that was input to sequencing final OtherMaterial library = - findLibraryFromProcessInput( + IsaJsonGraphLookup.findOtherMaterialFromProcessInput( sequencingProcess, assay.getMaterials()); if (library != null @@ -53,7 +52,7 @@ public void createENARunSetElement( final String experimentId = experimentSequenceMap.get(library.getId()); final List runDataFiles = - findDataFilesFromProcessOutputs( + IsaJsonGraphLookup.findDataFilesFromProcessOutputs( sequencingProcess, assay.getDataFiles()); createRunElement( runSetElement, @@ -68,87 +67,6 @@ 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; - } - - /** Resolves the assay data files referenced by a sequencing process outputs list. */ - private List findDataFilesFromProcessOutputs( - final ProcessSequence process, final List assayDataFiles) { - final List dataFiles = new ArrayList<>(); - if (process.getOutputs() == null || assayDataFiles == null) { - return dataFiles; - } - - for (final Output output : process.getOutputs()) { - if (output.getId() == null) { - continue; - } - - final String normalizedOutputId = normalizeDataFileId(output.getId()); - for (final DataFile dataFile : assayDataFiles) { - if (dataFile.getId() != null - && normalizeDataFileId(dataFile.getId()).equals(normalizedOutputId)) { - dataFiles.add(dataFile); - } - } - } - - return dataFiles; - } - /** Creates one ENA RUN element from all data files produced by a sequencing process. */ private void createRunElement( final Element runSetElement, From 7ae0327c3c3a329db7cc30848778cb55a9b3491d Mon Sep 17 00:00:00 2001 From: Bert Droesbeke Date: Mon, 13 Jul 2026 14:03:30 +0200 Subject: [PATCH 27/27] use multi test data in test --- .../WebinExperimentXmlCreatorTest.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) 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 808e3f95..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 @@ -266,11 +271,11 @@ void testExperimentMetadataCanComeFromOtherMaterialCharacteristics() throws Exce @Test void testCreateENAExperimentSetElementWithMultipleDataFiles() throws Exception { - // This test verifies that multiple data files from the same library - // only create one experiment (deduplication) + // 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 = isaJson.getInvestigation().getStudies(); + final List studies = multiIsaJson.getInvestigation().getStudies(); final String randomSubmissionIdentifier = "test-456"; final Map bioSampleAccessions = new HashMap<>(); bioSampleAccessions.put("SOURCE", "SAMEA130793922"); @@ -287,9 +292,17 @@ void testCreateENAExperimentSetElementWithMultipleDataFiles() throws Exception { // The number of experiments should match the number of unique libraries @SuppressWarnings("unchecked") final List experiments = experimentSet.elements("EXPERIMENT"); + Assertions.assertEquals( + 2, experimentSequence.size(), "Expected two unique libraries in the multi ISA fixture"); Assertions.assertEquals( experimentSequence.size(), experiments.size(), "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"); } }