diff --git a/pom.xml b/pom.xml index 19f2587..b9c9238 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,11 @@ picocli 4.6.2 + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.17.2 + jline jline diff --git a/src/main/java/life/qbic/io/PetabParser.java b/src/main/java/life/qbic/io/PetabParser.java new file mode 100644 index 0000000..220e41b --- /dev/null +++ b/src/main/java/life/qbic/io/PetabParser.java @@ -0,0 +1,97 @@ +package life.qbic.io; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import life.qbic.model.petab.PetabMetadata; + +public class PetabParser { + + private final String META_INFO_YAML = "metaInformation.yaml"; + + public PetabMetadata parse(String dataPath) { + + File directory = new File(dataPath); + List sourcePetabReferences = new ArrayList<>(); + + File yaml = findYaml(directory); + if (yaml != null) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(yaml)); + boolean inIDBlock = false; + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + // the id block ends, when a new key with colon is found + if (inIDBlock && line.contains(":")) { + inIDBlock = false; + } + // if we are in the id block, we collect one dataset code per line + if (inIDBlock) { + parseDatasetCode(line).ifPresent(sourcePetabReferences::add); + } + if (line.contains("openBISSourceIds:")) { + inIDBlock = true; + } + } + reader.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new PetabMetadata(sourcePetabReferences); + } + + private Optional parseDatasetCode(String line) { + // expected input: " - 20240702093837370-684137" + String[] tokens = line.split("-"); + if(tokens.length == 3) { + return Optional.of(tokens[1].strip()+"-"+tokens[2].strip()); + } else { + System.out.println("Could not extract dataset code from the following line:"); + System.out.println(line); + } + return Optional.empty(); + } + + public void addDatasetId(String outputPath, String datasetCode) throws IOException { + + Path path = Paths.get(Objects.requireNonNull(findYaml(new File(outputPath))).getPath()); + Charset charset = StandardCharsets.UTF_8; + + final String keyWord = "openBISId"; + + String idInLine = keyWord+":(.*)?(\\r\\n|[\\r\\n])"; + + String content = Files.readString(path, charset); + content = content.replaceAll(idInLine, keyWord+": "+datasetCode+"\n"); + Files.write(path, content.getBytes(charset)); + } + + private File findYaml(File directory) { + for (File file : Objects.requireNonNull(directory.listFiles())) { + if (file.isFile() && file.getName().equalsIgnoreCase(META_INFO_YAML)) { + return file; + } + if (file.isDirectory()) { + return findYaml(file); + } + } + System.out.println(META_INFO_YAML + " not found"); + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/life/qbic/io/commandline/CommandLineOptions.java b/src/main/java/life/qbic/io/commandline/CommandLineOptions.java index f2d1538..d050ebf 100644 --- a/src/main/java/life/qbic/io/commandline/CommandLineOptions.java +++ b/src/main/java/life/qbic/io/commandline/CommandLineOptions.java @@ -8,8 +8,8 @@ // main command with format specifiers for the usage help message @Command(name = "openbis-scripts", - subcommands = { SampleHierarchyCommand.class, FindDatasetsCommand.class, - UploadDatasetCommand.class, SpaceStatisticsCommand.class }, + subcommands = { SampleHierarchyCommand.class, FindDatasetsCommand.class, DownloadPetabCommand.class, + UploadPetabResultCommand.class, UploadDatasetCommand.class, SpaceStatisticsCommand.class }, description = "A client software for querying openBIS.", mixinStandardHelpOptions = true, versionProvider = ManifestVersionProvider.class) public class CommandLineOptions { diff --git a/src/main/java/life/qbic/io/commandline/DownloadDatasetCommand.java b/src/main/java/life/qbic/io/commandline/DownloadDatasetCommand.java deleted file mode 100644 index 102baff..0000000 --- a/src/main/java/life/qbic/io/commandline/DownloadDatasetCommand.java +++ /dev/null @@ -1,68 +0,0 @@ -package life.qbic.io.commandline; - -import ch.ethz.sis.openbis.generic.OpenBIS; -import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet; -import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; -import life.qbic.App; -import life.qbic.model.download.OpenbisConnector; -import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; - -@Command(name = "list-data", - description = "lists datasets and their details for a given experiment code") -public class DownloadDatasetCommand implements Runnable { - - @Parameters(arity = "1", paramLabel = "experiment", description = "The code of the experiment data is attached to") - private String experimentCode; - @Option(arity = "1", paramLabel = "", description = "Optional openBIS spaces to filter results", names = {"-s", "--space"}) - private String space; - @Mixin - AuthenticationOptions auth = new AuthenticationOptions(); - - @Override - public void run() { - List spaces = new ArrayList<>(); - if (space != null) { - System.out.println("Querying experiment in space: " + space + "..."); - spaces.add(space); - } else { - System.out.println("Querying experiment in all available spaces..."); - } - OpenBIS authentication = App.loginToOpenBIS(auth.getPassword(), auth.getUser(), auth.getAS()); - OpenbisConnector openbis = new OpenbisConnector(authentication); - List datasets = openbis.listDatasetsOfExperiment(spaces, experimentCode).stream() - .sorted(Comparator.comparing( - (DataSet d) -> d.getExperiment().getProject().getSpace().getCode())).collect( - Collectors.toList()); - int datasetIndex = 0; - for (DataSet dataSet : datasets) { - datasetIndex++; - System.out.println("["+datasetIndex+"]"); - System.out.println(dataSet.getExperiment().getIdentifier()); - System.out.println(dataSet.getCode()); - System.out.println(dataSet.getType().getCode()); - System.out.println(dataSet.getRegistrationDate()); - System.out.println(new SimpleDateFormat("MM-dd-yyyy").format(dataSet.getRegistrationDate())); - Person person = dataSet.getRegistrator(); - System.out.println(person.getFirstName() + " " + person.getLastName()); - System.out.println(); - } - } - - private String getTimeStamp() { - final String PATTERN_FORMAT = "YYYY-MM-dd_HHmmss"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN_FORMAT); - return LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC).format(formatter); - } -} diff --git a/src/main/java/life/qbic/io/commandline/DownloadPetabCommand.java b/src/main/java/life/qbic/io/commandline/DownloadPetabCommand.java new file mode 100644 index 0000000..f09d973 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/DownloadPetabCommand.java @@ -0,0 +1,60 @@ +package life.qbic.io.commandline; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import life.qbic.App; +import life.qbic.io.PetabParser; +import life.qbic.model.DatasetWithProperties; +import life.qbic.model.download.OpenbisConnector; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Parameters; + +@Command(name = "download-petab", + description = "Downloads PEtab dataset and stores some additional information from openbis in the petab.yaml") +public class DownloadPetabCommand implements Runnable { + + @Parameters(arity = "1", paramLabel = "dataset id", description = "The code of the dataset to download. Can be found via list-data.") + private String datasetCode; + @Parameters(arity = "1", paramLabel = "download path", description = "The local path where to store the downloaded data") + private String outputPath; + @Mixin + AuthenticationOptions auth = new AuthenticationOptions(); + + @Override + public void run() { + OpenBIS authentication = App.loginToOpenBIS(auth.getPassword(), auth.getUser(), auth.getAS(), auth.getDSS()); + OpenbisConnector openbis = new OpenbisConnector(authentication); + + List datasets = openbis.findDataSets(Collections.singletonList(datasetCode)); + + if(datasets.isEmpty()) { + System.out.println(datasetCode+" not found"); + return; + } + DatasetWithProperties result = new DatasetWithProperties(datasets.get(0)); + Optional patientID = openbis.findPropertyInSampleHierarchy("PATIENT_DKFZ_ID", + result.getExperiment().getIdentifier()); + patientID.ifPresent(s -> result.addProperty("patientID", s)); + + System.out.println("Found dataset, downloading."); + System.out.println(); + + openbis.downloadDataset(outputPath, datasetCode); + + PetabParser parser = new PetabParser(); + try { + System.out.println("Adding dataset identifier to metaInformation.yaml."); + parser.addDatasetId(outputPath, datasetCode); + } catch (IOException e) { + System.out.println("Could not add dataset identifier."); + throw new RuntimeException(e); + } + System.out.println("Done"); + } + +} diff --git a/src/main/java/life/qbic/io/commandline/UploadDatasetCommand.java b/src/main/java/life/qbic/io/commandline/UploadDatasetCommand.java index 9ea557e..74fa98e 100644 --- a/src/main/java/life/qbic/io/commandline/UploadDatasetCommand.java +++ b/src/main/java/life/qbic/io/commandline/UploadDatasetCommand.java @@ -70,9 +70,4 @@ private boolean pathValid(String dataPath) { return new File(dataPath).exists(); } - private String getTimeStamp() { - final String PATTERN_FORMAT = "YYYY-MM-dd_HHmmss"; - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN_FORMAT); - return LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC).format(formatter); - } } diff --git a/src/main/java/life/qbic/io/commandline/UploadPetabResultCommand.java b/src/main/java/life/qbic/io/commandline/UploadPetabResultCommand.java new file mode 100644 index 0000000..3f29163 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/UploadPetabResultCommand.java @@ -0,0 +1,79 @@ +package life.qbic.io.commandline; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId; +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import life.qbic.App; +import life.qbic.io.PetabParser; +import life.qbic.model.download.OpenbisConnector; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Parameters; + +@Command(name = "upload-petab", + description = "uploads a PETab folder and attaches it to a provided experiment and any datasets referenced in the PETab metadata (e.g. for PETab results).") +public class UploadPetabResultCommand implements Runnable { + + @Parameters(arity = "1", paramLabel = "file/folder", description = "The path to the file or folder to upload") + private String dataPath; + @Parameters(arity = "1", paramLabel = "experiment ID", description = "The full identifier of the experiment the data should be attached to. " + + "The identifier must be of the format: /space/project/experiment") + private String experimentID; + //@Option(arity = "1..*", paramLabel = "", description = "Optional list of dataset codes to act" + // + " as parents for the upload. E.g. when this dataset has been generated using these datasets as input.", names = {"-pa", "--parents"}) + private List parents = new ArrayList<>(); + @Mixin + AuthenticationOptions auth = new AuthenticationOptions(); + + private OpenbisConnector openbis; + private PetabParser petabParser = new PetabParser(); + + @Override + public void run() { + OpenBIS authentication = App.loginToOpenBIS(auth.getPassword(), auth.getUser(), auth.getAS(), auth.getDSS()); + openbis = new OpenbisConnector(authentication); + + if(!pathValid(dataPath)) { + System.out.printf("Path %s could not be found%n", dataPath); + return; + } + if(!experimentExists(experimentID)) { + System.out.printf("Experiment %s could not be found%n", experimentID); + return; + } + System.out.println("Looking for reference datasets in metaInformation.yaml..."); + parents = petabParser.parse(dataPath).getSourcePetabReferences(); + if(parents.isEmpty()) { + System.out.println("No reference datasets found in openBISSourceIds property. Assuming" + + "this is a new dataset."); + } else { + System.out.println("Found reference ids: " + String.join(", ", parents)); + if (!datasetsExist(parents)) { + System.out.printf("One or more datasets %s could not be found%n", parents); + return; + } else { + System.out.println("Referenced datasets found"); + } + } + System.out.println("Uploading dataset..."); + //TODO copy and remove source references here + DataSetPermId result = openbis.registerDataset(Path.of(dataPath), experimentID, parents); + System.out.printf("Dataset %s was successfully created%n", result.getPermId()); + } + + private boolean datasetsExist(List datasetCodes) { + return openbis.findDataSets(datasetCodes).size() == datasetCodes.size(); + } + + private boolean experimentExists(String experimentID) { + return openbis.experimentExists(experimentID); + } + + private boolean pathValid(String dataPath) { + return new File(dataPath).exists(); + } + +} diff --git a/src/main/java/life/qbic/model/petab/Arguments.java b/src/main/java/life/qbic/model/petab/Arguments.java new file mode 100644 index 0000000..05629ed --- /dev/null +++ b/src/main/java/life/qbic/model/petab/Arguments.java @@ -0,0 +1,16 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class Arguments { + @JsonProperty + List housekeeperObservableIds; + + @Override + public String toString() { + return "Arguments{" + + "housekeeperObservableIds=" + housekeeperObservableIds + + '}'; + } +} diff --git a/src/main/java/life/qbic/model/petab/CellCountInfo.java b/src/main/java/life/qbic/model/petab/CellCountInfo.java new file mode 100644 index 0000000..e65ce59 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/CellCountInfo.java @@ -0,0 +1,23 @@ +package life.qbic.model.petab; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CellCountInfo { + @JsonProperty + double seeded; + @JsonProperty + String ncellsCount; + @JsonProperty + String unit; + + @Override + public String toString() { + return "CellCountInfo{" + + "seeded=" + seeded + + ", ncellsCount='" + ncellsCount + '\'' + + ", unit='" + unit + '\'' + + '}'; + } +} + diff --git a/src/main/java/life/qbic/model/petab/ConditionWithUnit.java b/src/main/java/life/qbic/model/petab/ConditionWithUnit.java new file mode 100644 index 0000000..d46e925 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/ConditionWithUnit.java @@ -0,0 +1,26 @@ +package life.qbic.model.petab; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ConditionWithUnit { + @JsonProperty + String name; + @JsonProperty + String unit; + + public ConditionWithUnit() {} + + public ConditionWithUnit(String name, String unit) { + this.name = name; + this.unit = unit; + } + + @Override + public String toString() { + return "ConditionWithUnit{" + + "name='" + name + '\'' + + ", unit='" + unit + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/life/qbic/model/petab/ExperimentalCondition.java b/src/main/java/life/qbic/model/petab/ExperimentalCondition.java new file mode 100644 index 0000000..8672a80 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/ExperimentalCondition.java @@ -0,0 +1,35 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + + +public class ExperimentalCondition { + @JsonProperty + IdWithPattern conditionId; + @JsonProperty + List conditions; + + public ExperimentalCondition() {} + + public ExperimentalCondition(IdWithPattern pattern, List conditions) { + this.conditionId = pattern; + this.conditions = conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + public void setConditionId(IdWithPattern id) { + this.conditionId = id; + } + + @Override + public String toString() { + return "ExperimentalCondition{" + + "conditionId=" + conditionId + + ", conditions=" + conditions + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/life/qbic/model/petab/IdWithPattern.java b/src/main/java/life/qbic/model/petab/IdWithPattern.java new file mode 100644 index 0000000..98ab352 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/IdWithPattern.java @@ -0,0 +1,22 @@ +package life.qbic.model.petab; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class IdWithPattern { + @JsonProperty + String pattern; + + public IdWithPattern() {} + + public IdWithPattern(String pattern) { + this.pattern = pattern; + } + + @Override + public String toString() { + return "IdWithPattern{" + + "pattern='" + pattern + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/life/qbic/model/petab/Measurement.java b/src/main/java/life/qbic/model/petab/Measurement.java new file mode 100644 index 0000000..531e7b5 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/Measurement.java @@ -0,0 +1,19 @@ +package life.qbic.model.petab; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Measurement { + @JsonProperty + String unit; + @JsonProperty + String lloq; + + @Override + public String toString() { + return "Measurement{" + + "unit='" + unit + '\'' + + ", lloq='" + lloq + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/life/qbic/model/petab/MeasurementData.java b/src/main/java/life/qbic/model/petab/MeasurementData.java new file mode 100644 index 0000000..62b449a --- /dev/null +++ b/src/main/java/life/qbic/model/petab/MeasurementData.java @@ -0,0 +1,21 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class MeasurementData { + @JsonProperty + private Measurement measurement; + @JsonProperty + private Time time; + @JsonProperty + private IdWithPattern replicateId; + + @Override + public String toString() { + return "MeasurementData{" + + "measurement=" + measurement + + ", time=" + time + + ", replicateId=" + replicateId + + '}'; + } +} diff --git a/src/main/java/life/qbic/model/petab/Medium.java b/src/main/java/life/qbic/model/petab/Medium.java new file mode 100644 index 0000000..d5c4ed3 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/Medium.java @@ -0,0 +1,22 @@ +package life.qbic.model.petab; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Medium { + @JsonProperty + String type; + @JsonProperty + double volume; + @JsonProperty + String unit; + + @Override + public String toString() { + return "Medium{" + + "type='" + type + '\'' + + ", volume=" + volume + + ", unit='" + unit + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/life/qbic/model/petab/MetaInformation.java b/src/main/java/life/qbic/model/petab/MetaInformation.java new file mode 100644 index 0000000..ff19ab3 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/MetaInformation.java @@ -0,0 +1,90 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class MetaInformation { + + @JsonProperty + private ExperimentInformation ExperimentInformation; + + @JsonProperty + private Units units; + @JsonProperty + private PreprocessingInformation PreprocessingInformation; + @JsonProperty + private MeasurementData measurementData; + @JsonProperty + private ExperimentalCondition experimentalCondition; + + + @Override + public String toString() { + return "MetaInformation{" + + "units=" + units + + ", preprocessingInformation=" + PreprocessingInformation + + ", measurementData=" + measurementData + + ", experimentalCondition=" + experimentalCondition + + '}'; + } + + public Units getUnits() { + return units; + } + + public class ExperimentInformation { + + @Override + public String toString() { + return "MetaInformation{}"; + } + } + + public class Units { + @JsonProperty + private String measurement; + @JsonProperty + private String time; + @JsonProperty + private String treatment; + @JsonProperty + private String stimulus; + @JsonProperty + private Medium medium; + @JsonProperty + private CellCountInfo ncells; + @JsonProperty + private String measurement_technique; + @JsonProperty + private String openBISId; + @JsonProperty + private List openBISParentIds; + @JsonProperty + private List dateOfExperiment; + + @Override + public String toString() { + return "Units{" + + "measurement='" + measurement + '\'' + + ", time='" + time + '\'' + + ", treatment='" + treatment + '\'' + + ", stimulus='" + stimulus + '\'' + + ", medium=" + medium + + ", ncells=" + ncells + + ", measurement_technique='" + measurement_technique + '\'' + + ", openBISId='" + openBISId + '\'' + + ", openBISParentIds=" + openBISParentIds + + ", dateOfExperiment=" + dateOfExperiment + + '}'; + } + + public void setOpenbisParentIds(List list) { + this.openBISParentIds = list; + } + + public void setOpenbisId(String id) { + this.openBISId = id; + } + } + +} diff --git a/src/main/java/life/qbic/model/petab/PetabMetadata.java b/src/main/java/life/qbic/model/petab/PetabMetadata.java new file mode 100644 index 0000000..7fb8b01 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/PetabMetadata.java @@ -0,0 +1,16 @@ +package life.qbic.model.petab; + +import java.util.List; + +public class PetabMetadata { + + List sourceDatasetIdentifiers; + + public PetabMetadata(List sourceDatasetIdentifiers) { + this.sourceDatasetIdentifiers = sourceDatasetIdentifiers; + } + + public List getSourcePetabReferences() { + return sourceDatasetIdentifiers; + } +} diff --git a/src/main/java/life/qbic/model/petab/Preprocessing.java b/src/main/java/life/qbic/model/petab/Preprocessing.java new file mode 100644 index 0000000..6bc4dbf --- /dev/null +++ b/src/main/java/life/qbic/model/petab/Preprocessing.java @@ -0,0 +1,21 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Preprocessing { + @JsonProperty + private String method; + @JsonProperty + private String description; + @JsonProperty + private Arguments arguments; + + @Override + public String toString() { + return "Preprocessing{" + + "method='" + method + '\'' + + ", description='" + description + '\'' + + ", arguments=" + arguments + + '}'; + } +} diff --git a/src/main/java/life/qbic/model/petab/PreprocessingInformation.java b/src/main/java/life/qbic/model/petab/PreprocessingInformation.java new file mode 100644 index 0000000..b7a2290 --- /dev/null +++ b/src/main/java/life/qbic/model/petab/PreprocessingInformation.java @@ -0,0 +1,18 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PreprocessingInformation { + @JsonProperty + private String normalizationStatus; + @JsonProperty + private Preprocessing preprocessing; + + @Override + public String toString() { + return "PreprocessingInformation{" + + "normalizationStatus='" + normalizationStatus + '\'' + + ", preprocessing=" + preprocessing + + '}'; + } +} diff --git a/src/main/java/life/qbic/model/petab/Time.java b/src/main/java/life/qbic/model/petab/Time.java new file mode 100644 index 0000000..ce9ea4f --- /dev/null +++ b/src/main/java/life/qbic/model/petab/Time.java @@ -0,0 +1,15 @@ +package life.qbic.model.petab; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Time { + @JsonProperty + String unit; + + @Override + public String toString() { + return "Time{" + + "unit='" + unit + '\'' + + '}'; + } +}