diff --git a/.github.settings.xml b/.github.settings.xml new file mode 100644 index 0000000..5c65a2e --- /dev/null +++ b/.github.settings.xml @@ -0,0 +1,14 @@ + + + + nexus-snapshots + ${env.MAVEN_REPO_USERNAME} + ${env.MAVEN_REPO_PASSWORD} + + + nexus-releases + ${env.MAVEN_REPO_USERNAME} + ${env.MAVEN_REPO_PASSWORD} + + + \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..8d7e69b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @qbicsoftware/itss diff --git a/.github/pr-labels.yml b/.github/pr-labels.yml new file mode 100644 index 0000000..f95cf6e --- /dev/null +++ b/.github/pr-labels.yml @@ -0,0 +1,3 @@ +feature: ['feature/*', 'feat/*'] +fix: ['fix/*', 'hotfix'] +chore: ['chore/*', 'documentation/*', 'docs/*', 'ci/*', 'refactor/*'] diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..1a928f0 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,20 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: + - JohnnyQ5 + - github-actions + categories: + - title: New Features πŸš€ + labels: + - feature + - title: Bugfixes πŸͺ² + labels: + - fix + - title: Documentation & CI πŸͺ‚ + labels: + - chore + - title: Others πŸ§ƒ + labels: + - "*" diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml new file mode 100644 index 0000000..15172ce --- /dev/null +++ b/.github/workflows/build-package.yml @@ -0,0 +1,29 @@ +name: Build Maven Package + +on: + push: + branches: + - '**' + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, master ] + +jobs: + package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Load local Maven repository cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Run mvn package + run: mvn -B package --file pom.xml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..7951f13 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,83 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, master, development, release/*, hotfix/* ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, master ] + schedule: + - cron: '21 1 * * 4' + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '11' + settings-path: ${{ github.workspace }} + + - name: Load local Maven repository cache + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..058427c --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,96 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + versionTag: + description: 'Version Tag (semantic version)' + required: true + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + settings-path: ${{ github.workspace }} + + - name: Load local Maven repository cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Set up git + run: | + git config --global user.email "support@qbic.zendesk.com" + git config --global user.name "JohnnyQ5" + + - name: Set version in Maven project + run: mvn versions:set -DnewVersion=${{ github.event.inputs.versionTag }} + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Create Release Notes + if: ${{ !startsWith(github.ref, 'refs/tags/') + && !( contains(github.event.inputs.versionTag, 'alpha') + || contains(github.event.inputs.versionTag, 'beta') + || contains(github.event.inputs.versionTag, 'rc')) }} + uses: actions/github-script@v4.0.2 + with: + github-token: ${{secrets.JOHNNY_Q5_REPORTS_TOKEN}} + script: | + await github.request(`POST /repos/${{ github.repository }}/releases`, { + tag_name: "${{ github.event.inputs.versionTag }}", + generate_release_notes: true + }); + + - name: Create Pre-Release Notes + if: ${{ !startsWith(github.ref, 'refs/tags/') + && ( contains(github.event.inputs.versionTag, 'alpha') + || contains(github.event.inputs.versionTag, 'beta') + || contains(github.event.inputs.versionTag, 'rc')) }} + uses: actions/github-script@v4.0.2 + with: + github-token: ${{secrets.JOHNNY_Q5_REPORTS_TOKEN}} + script: | + await github.request(`POST /repos/${{ github.repository }}/releases`, { + tag_name: "${{ github.event.inputs.versionTag }}", + generate_release_notes: true, + prerelease: true + }); + + - name: Publish artefact to QBiC Nexus Repository + run: mvn --quiet --settings $GITHUB_WORKSPACE/.github.settings.xml deploy + env: + MAVEN_REPO_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_REPO_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + + - name: Switch to new branch + run: git checkout -b release/set-version-to-${{ github.event.inputs.versionTag }} + + - name: Set remote branch + run: git push --set-upstream origin release/set-version-to-${{ github.event.inputs.versionTag }} + + - name: Checkin commit + run: git commit . -m 'Set version to ${{ github.event.inputs.versionTag }}' + + - name: Push to Github + run: git push + + - name: Open PR with version bump + uses: actions/github-script@v4.0.2 + with: + github-token: ${{secrets.JOHNNY_Q5_REPORTS_TOKEN}} + script: | + await github.request(`POST /repos/${{ github.repository }}/pulls`, { + title: 'Update version to ${{ github.event.inputs.versionTag }}', + head: 'release/set-version-to-${{ github.event.inputs.versionTag }}', + base: 'master' + }); diff --git a/.github/workflows/label-pull-requests.yml b/.github/workflows/label-pull-requests.yml new file mode 100644 index 0000000..b03eee1 --- /dev/null +++ b/.github/workflows/label-pull-requests.yml @@ -0,0 +1,15 @@ +name: Label Pull Requests + +on: + pull_request: + types: [ opened, edited ] + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: TimonVS/pr-labeler-action@v3 + with: + configuration-path: .github/pr-labels.yml # optional, .github/pr-labeler.yml is the default value + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/nexus-publish-snapshots.yml b/.github/workflows/nexus-publish-snapshots.yml new file mode 100644 index 0000000..979529c --- /dev/null +++ b/.github/workflows/nexus-publish-snapshots.yml @@ -0,0 +1,46 @@ +# This workflow will build a package using Maven and then publish it to +# qbic-repo.qbic.uni-tuebingen.de packages when a release is created +# For more information see: https://github.com/actions/setup-java#apache-maven-with-a-settings-path + +name: Deploy Snapshot + +on: + push: + branches: + - development + +jobs: + publish_snapshots: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + settings-path: ${{ github.workspace }} + + - name: Load local Maven repository cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + # Remove existing snapshot tags which are not supposed to be present + - name: Remove snapshot tags + run: mvn versions:set -DremoveSnapshot + # Set the SNAPSHOT for this build and deployment + - name: Set version in Maven project + run: mvn versions:set -DnewVersion='${project.version}-SNAPSHOT' + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish artefact to QBiC Nexus Repository + run: mvn --settings $GITHUB_WORKSPACE/.github.settings.xml deploy + env: + MAVEN_REPO_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_REPO_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..174cef2 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,31 @@ +name: Run Maven Tests + +on: + push: + branches: + - '**' + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Load local Maven repository cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Run tests + run: mvn clean verify diff --git a/LICENSE b/LICENSE index 5083f44..d167600 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 QBiC +Copyright (c) 2018-2024 University of TΓΌbingen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..eedc8cc --- /dev/null +++ b/README.md @@ -0,0 +1,418 @@ +# OpenBIS Scripts User Documentation + +## OpenBIS Statistics and Data Model + +In order to interact with openBIS, the parameters for the application server URL, the user and their password, have to be provided. + +In order to interact with data in openBIS (upload or download), a data store (dss) URL needs to be provided, as well. + +**Everything but the password can be provided via the config file (config.txt):** + +* as=https://my-openbis-instance.de/openbis/openbis +* dss=https://my-openbis-instance.de/datastore_server +* user=your-username + +Keep in mind that you have to edit the config file or provide these parameters via command line, if you want to use different users or connect to different openBIS instances. + +Refer to the help of the respective command or the examples below for more details. + +### Finding Datasets + +The list-data command can be used to list Datasets in openBIS and some of their metadata based on the experiment or sample they are attached to. Experiments or samples are specified by their openBIS code or identifier. + +The optional 'space' parameter can be used to only show datasets found in the provided space. + +**Example command:** + +`java -jar scripts.jar list-data /SPACY/PROJECTX/TEST_PATIENTS1 -config config.txt --openbis-pw` + +**Example output:** + + reading config + Querying experiment in all available spaces... + Query is not an object code, querying for: TEST_PATIENTS1 instead. + [main] INFO org.eclipse.jetty.util.log - Logging initialized @6997ms to org.eclipse.jetty.util.log.Slf4jLog + Found 4 datasets for experiment TEST_PATIENTS1: + [1] + patientIDs: ID 4930-72,ID 4931-79 + ID: 20241014000813459-189089 (/TEST_PATIENTS1) + Type: UNKNOWN + Uploaded by Friedrich Andreas (10-14-24 20:58:13) + + [2] + patientIDs: ID 4930-72,ID 4931-79 + ID: 20241000010001025-189090 (/SPACY/RPOJECTX/TEST_PATIENTS1) + Type: UNKNOWN + Uploaded by Friedrich Andreas (10-14-24 21:00:01) + +### Showing Space Statistics + +The Statistics command can be used to list the number of collections, sample objects and attached datasets by type for one or all spaces accessible by the user. + +The --space command can be used to only show the objects in a specific openBIS space. + +An output file for the resulting list can be specified using the --out command. + +By default, openBIS settings objects and material spaces are ignored. This can be overwritten using --show-settings. + +**Example command:** + +`java -jar scripts.jar statistics -config config.txt --openbis-pw` + +**Example output:** + + Querying samples in all available spaces... + ----- + Summary for TEMP_PLAYGROUND + ----- + Experiments (9): + + 02_MASSSPECTROMETRY_EXPERIMENT: 1 + 00_STANDARD_OPERATING_PROTOCOLS: 1 + 00_PATIENT_DATABASE: 3 + 01_BIOLOGICAL_EXPERIMENT: 4 + + Samples (316): + + 00_PRIMARY_BLOOD: 128 + 03_SDS_PAGE_SETUP: 1 + 00_PRIMARY_TISSUE: 24 + PLASMID: 161 + 02_EXPERIMENT_TREATMENT: 2 + + Attached datasets (30): + + IB_DATA: 4 + SOURCE_CODE: 2 + RAW_DATA: 1 + EXPERIMENT_RESULT: 3 + MS_DATA_RAW: 1 + UNKNOWN: 18 + EXPERIMENT_PROTOCOL: 1 + +### Showing Sample Hierarchy + +The Sample Types command queries all sample types and prints which types are connected and how often (via samples existing in the queried openBIS instance), creating a sample type hierarchy. + +The --space option can be used to only show the sample-types used in a specific openBIS space. + +An output file for the resulting hierarchy can be specified using the --out command. + +**Example command:** + +`java -jar scripts.jar statistics -config config.txt --openbis-pw` + +**Example output:** + + Querying samples in all available spaces... + MATERIAL.CHEMICALS (1) + PATIENT_ID (1) + PATIENT_ID -> PATIENT_ID (1) + 05_MS_RUN (1) + 00_PATIENT_INFO -> 01_EXPERIMENT_PRIMARYCULTURE (3) + 04_IMMUNOBLOT (4) + 03_SDS_PAGE_SETUP -> 04_IMMUNOBLOT (4) + +## Upload/Download and Interaction with PEtab + +### Uploading general data + +The Upload Dataset command can be used to upload a Dataset to openBIS and connect it to existing +datasets. + +To upload a dataset, the path to the file or folder and the object ID to which it should be attached +need to be provided. Objects can be experiments or samples. + +Parent datasets can be specified using the --parents command. + +If the specified object ID or any of the specified parent datasets cannot be found, the script will +stop and return an error message. + +The dataset type of the new dataset in openBIS can be specified using the --type option, otherwise +the type "UNKNOWN" will be used. + +**Example command:** + +`java -jar scripts.jar upload-data README.md /SPACY/PROJECTX/MY_SAMPLE -t ATTACHMENT -config config.txt --openbis-pw` + +**Example output:** + + Parameters verified, uploading dataset... + + Dataset 20241021125328024-689105 was successfully attached to experiment` + +### Downloading a PEtab dataset + +The Download PEtab command can be used to download a PEtab Dataset from openBIS and store some +additional information from openBIS in the metaInformation.yaml file (or a respective yaml file +containing 'metaInformation' in its name). + +The Dataset to download is specified by providing the openBIS dataset identifier (code) and the +PEtab is downloaded to the download path parameter provided. + +By design, the Dataset Identifier is added to the downloaded metaInformation.yaml as 'openBISId' +in order to keep track of the source of this PEtab. + +### Uploading a PEtab dataset + +The Upload PEtab command can be used to upload a PEtab Dataset to openBIS and connect it to its +source files if these are stored in the same openBIS instance and referenced in the PEtabs metadata. + +To upload a PEtab dataset, the path to the PEtab folder and the experiment ID to which it should be +attached need to be provided. + +The dataset type of the new dataset in openBIS can be specified using the --type option, otherwise +the type "UNKNOWN" will be used. + +The script will search the **metaInformation.yaml** for the entry "**openBISSourceIds:**" and attach +the new dataset to all the datasets with ids in the following blocks found in this instance of +openBIS: + + openBISSourceIds: + - 20210702093837370-184137 + - 20220702100912333-189138 + +If one or more dataset identifiers are not found, the script will stop without uploading the data +and inform the user. + +## Interaction with SEEK instances + +In order to interact with SEEK, the parameters for the server URL, the user (usually an email +address) and their password, have to be provided. + +In order to interact with openBIS (transfer of data and metadata), the respective credentials need +to be provided, as well. + +**Everything but the passwords can be provided via the config file (e.g. config.txt):** + +* as=https://my-openbis-instance.de/openbis/openbis +* dss=https://my-openbis-instance.de/datastore_server +* user=your-openbis-username +* seek_user=your@email.de +* seek_url=http://localhost:3000 + +**Furthermore, names of default project and investigation in SEEK can be provided:** + +* seek_default_project=seek_test +* seek_default_investigation=default_investigation + +In order to keep track of samples transferred from openBIS, the script will try to transfer the +openBIS identifier of each sample to an additional SEEK sample type attribute (more details in the +section **Transferring Sample Types to SEEK**). + +**Its name can be specified in the config:** + +* seek_openbis_sample_title=openBIS Name + +In order to create certain objects in SEEK, user-provided mappings need to be provided via property +files placed in the same folder as the .jar. These are: + +**experiment_type_to_assay_class.properties** + +Here, openBIS experiment type codes are mapped to the assay class needed to create assay objects in +SEEK. Example entries: + + PARAMETER_ESTIMATION=EXP + MASS_SPECTROMETRY_EXPERIMENT=EXP + +**experiment_type_to_assay_type.properties** + +Here, openBIS experiment type codes are mapped to the assay type property needed to create assay +objects in SEEK. Example entry: + + MASS_SPECTROMETRY_EXPERIMENT=http://jermontology.org/ontology/JERMOntology#Proteomics + +Other fitting assay types can be found using the JERM ontology browser: +https://bioportal.bioontology.org/ontologies/JERM/?p=classes&conceptid=http%3A%2F%2Fjermontology.org%2Fontology%2FJERMOntology%23Experimental_assay_type&lang=en + +**dataset_type_to_asset_type.properties** + +Here, openBIS dataset type codes are mapped to the asset type created in SEEK. Example entries: + + SOURCE_CODE=documents + UNKNOWN=data_files + +Refer to the help of the respective command or the examples below for more details. + +### Transferring Sample Types to SEEK + +The Sample Type Transfer command transfers sample types and their attributes from an openBIS to a +SEEK instance. + +In order to do this, a mapping of the respective data types/sample attribute types needs to be +specified. This can be found (and changed) in the provided property file +**openbis_datatype_to_seek_attributetype.xml**: + + + 3 + Real number + Float + + +Each **entry type** denotes the **data type** in openBIS. Note that SEEK needs the identifier of the +respective sample attribute type (here: *3* for *Float*) in its database. Any changes to the file +need to reflect this. + +The available types and their identifiers can be queried at the endpoint **sample_attribute_types**. +For example: +`http://localhost:3000/sample_attribute_types` + +When transferring openBIS sample types, the command will automatically add a mandatory title +attribute to the sample type in SEEK. This title will be filled with the identifier of the openBIS +**sample object** (not sample type!) will be added. The attribute name is specified in the config +file and should selected before sample types are transferred to the respective instance: +* seek_openbis_sample_title=openBIS Name + +By default, only sample types (not samples!) with names not already found in SEEK will be +transferred and the user will be informed if duplicates are found. The option **--ignore-existing** +can be used to transfer existing sample types a second time, although it is recommended to only use +this option for testing purposes. + +### Transferring openBIS objects and files to SEEK + +The OpenBIS to Seek command transfers metadata and (optionally) data from openBIS to SEEK. +Experiments, samples and dataset information are always transferred together (as assays, samples and +one of several **asset types** in SEEK). + +The script will try to find the provided **openbis ID** in experiments, samples or datasets and +fetch any missing information to create a SEEK node containing at least one assay (when an +experiment without samples and datasets is specified). + +The seek-study needs to be provided to attach the assay. TODO: This information is also used to +decide if the node(s) should be updated (if they exist for the provided study) or created anew. + +Similarly, the title of the project in SEEK where nodes should be added, can either be provided via +the config file as **'seek_default_project'** or via the command line using the **--seek-project** +option. + +Info in the created asset(s) always links back to the openBIS path of the respective dataset. +The data itself can be transferred and stored in SEEK using the '-d' flag. + +To completely exclude some dataset information from being transferred, a file ('--blacklist') +containing dataset codes (from openBIS) can be specified. //TODO do this for samples/sample types + +In order to store links to the newly created SEEK objects in the source openBIS instance, the +following sample type is needed: + + Sample Type Code: EXTERNAL_LINK + Property: LINK_TYPE (VARCHAR) + Property: URL (VARCHAR) + +EXTERNAL_LINK samples are added to transferred experiments and samples and point to their respective +counterparts in SEEK. If the sample type is not available, this will be logged. + +### Updating nodes in SEEK based on updates in openBIS + +Updating nodes in SEEK uses the same general command, parameters and options. Unless otherwise +specified (**--no-update** flag), the command will try to update existing nodes in SEEK (recognized +by openBIS identifiers in their metadata, as well as the provided study name). + +The updating of a node-structure is done based on the following rules: +1. if an assay contains the openBIS permID of the experiment AND is attached to specified study, +its samples and assets are updated +2. samples are created, if the openBIS name of the respective sample is not found in a sample +attached to the assay in question +3. samples are updated, if their openBIS name is found in a sample attached to the asset and at +least one sample attribute is different in openBIS and SEEK +4. assets attached to the experiment or samples will be created, if they are missing from this assay +5. no existing sample or assets are deleted from SEEK, even if they are missing from openBIS + +**Example command:** + +`java -jar scripts.jar openbis-to-seek /MYSPACE/PROJECTY/00_P_INFO_691 mystudy -d -config config.txt --openbis-pw --seek-pw` + +**Example output:** + + Transfer openBIS -> SEEK started. + Provided openBIS object: /MYSPACE/PROJECTY/00_P_INFO_691 + Provided SEEK study title: mystudy + No SEEK project title provided, will search config file. + Transfer datasets to SEEK? true + Update existing nodes if found? true + Connecting to openBIS... + Searching for specified object in openBIS... + Search successful. + Connecting to SEEK... + Collecting information from openBIS... + Translating openBIS property codes to SEEK names... + Creating SEEK structure... + Trying to find existing corresponding assay in SEEK... + Found assay with id 64 + Updating nodes... + Mismatch found in Gender attribute of /MYSPACE/PROJECTY/00_P_INFO_691. Sample will be updated. + http://localhost:3000/assays/64 was successfully updated. + +#### RO-Crates + +While the creation of RO-Crates is not fully implemented, the command creates a folder and metadata +structure based on OpenBIS experiment, sample and dataset information. The command works similarly +to the OpenBIS to Seek command, with the difference that no SEEK instance and fewer mapping +parameters need to be provided (there will be no references to existing study or project objects in +SEEK). + +The script will try to find the provided **openbis ID** in experiments, samples or datasets and +fetch any missing information to create a folder structure in the provided **ro-path** containing at +least one assay's information (when an experiment without samples and datasets is specified). + +Assets (files and their ISA metadata) are stored in a folder named like the openBIS dataset code +they are part of, which is either the subfolder of the experiment (assay), or the subfolder of a +sample, depending on where the dataset was attached in openBIS. + +Info in the created asset .jsons always links back to the openBIS path of the respective dataset. +The data itself can be downloaded into the structure using the '-d' flag. + +To completely exclude some dataset information from being transferred, a file ('--blacklist') +containing dataset codes (from openBIS) can be specified. //TODO do this for samples/sample types + +**Example command:** + +`java -jar scripts.jar ro-crate /TEMP_PLAYGROUND/TEMP_PLAYGROUND/TEST_PATIENTS1 my-ro-crate -config config.txt --openbis-pw -d` + +**Example output:** + + reading config + Transfer openBIS -> RO-crate started. + Provided openBIS object: /TEMP_PLAYGROUND/TEMP_PLAYGROUND/TEST_PATIENTS1 + Pack datasets into crate? true + Connecting to openBIS... + Searching for specified object in openBIS... + Search successful. + Collecting information from openBIS... + Translating openBIS structure to ISA structure... + Writing assay json for /TEMP_PLAYGROUND/TEMP_PLAYGROUND/TEST_PATIENTS1. + Writing sample json for /TEMP_PLAYGROUND/TEMP_PLAYGROUND/00_P_INFO_670490. + Writing sample json for /TEMP_PLAYGROUND/TEMP_PLAYGROUND/00_P_INFO_670491. + Writing asset json for file in dataset 20241014205813459-689089. + Downloading dataset file to asset folder. + Writing asset json for file in dataset 20241014210001025-689090. + Downloading dataset file to asset folder. + Writing asset json for file in dataset 20241014205813459-689089. + Downloading dataset file to asset folder. + Writing asset json for file in dataset 20241021191109163-689109. + Downloading dataset file to asset folder. + ... + +**Creates structure:** + + my-ro-crate + └── TEMP_PLAYGROUND_TEMP_PLAYGROUND_TEST_PATIENTS1 + β”œβ”€β”€ 20241021125328024-689105 + β”‚ β”œβ”€β”€ README.md + β”‚ └── README.md.json + β”œβ”€β”€ TEMP_PLAYGROUND_TEMP_PLAYGROUND_00_P_INFO_670490 + β”‚ └── TEMP_PLAYGROUND_TEMP_PLAYGROUND_00_P_INFO_670490.json + β”œβ”€β”€ TEMP_PLAYGROUND_TEMP_PLAYGROUND_00_P_INFO_670491 + β”‚ β”œβ”€β”€ 20241014210317842-689092 + β”‚ β”‚ β”œβ”€β”€ scripts-new.jar + β”‚ β”‚ └── scripts-new.jar.json + β”‚ β”œβ”€β”€ 20241021173011602-689108 + β”‚ β”‚ └── smol_petab + β”‚ β”‚ β”œβ”€β”€ metaInformation.yaml + β”‚ β”‚ └── metaInformation.yaml.json + β”‚ β”œβ”€β”€ 20241021191109163-689109 + β”‚ β”‚ β”œβ”€β”€ testfile_100 + β”‚ β”‚ └── testfile_100.json + β”‚ └── TEMP_PLAYGROUND_TEMP_PLAYGROUND_00_P_INFO_670491.json + └── TEMP_PLAYGROUND_TEMP_PLAYGROUND_TEST_PATIENTS1.json + +## Caveats and Future Options diff --git a/dataset_type_to_asset_type.properties b/dataset_type_to_asset_type.properties new file mode 100644 index 0000000..44bb45f --- /dev/null +++ b/dataset_type_to_asset_type.properties @@ -0,0 +1,3 @@ +SOURCE_CODE=documents +UNKNOWN=data_files +TEST_DAT=data_files diff --git a/experiment_type_to_assay_class.properties b/experiment_type_to_assay_class.properties new file mode 100644 index 0000000..a37900e --- /dev/null +++ b/experiment_type_to_assay_class.properties @@ -0,0 +1,2 @@ +PARAMETER_ESTIMATION=EXP +MASS_SPECTROMETRY_EXPERIMENT=EXP diff --git a/experiment_type_to_assay_type.properties b/experiment_type_to_assay_type.properties new file mode 100644 index 0000000..5ee82f3 --- /dev/null +++ b/experiment_type_to_assay_type.properties @@ -0,0 +1 @@ +MASS_SPECTROMETRY_EXPERIMENT=http://jermontology.org/ontology/JERMOntology#Proteomics diff --git a/openbis_datatype_to_seek_attributetype.xml b/openbis_datatype_to_seek_attributetype.xml new file mode 100644 index 0000000..0a29815 --- /dev/null +++ b/openbis_datatype_to_seek_attributetype.xml @@ -0,0 +1,64 @@ + + + + 4 + Integer + Integer + + + 8 + String + String + + + 7 + Text + Text + + + 3 + Real number + Float + + + 1 + Date time + DateTime + + + 16 + Boolean + Boolean + + + 8 + String + String + + + 8 + String + String + + + 8 + String + String + + + 7 + Text + Text + + + 5 + Web link + String + + + 2 + Date time + Date + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b9c9238 --- /dev/null +++ b/pom.xml @@ -0,0 +1,155 @@ + + + 4.0.0 + life.qbic + openbis-scripts + 1.0.0 + OpenBis Scripts + https://github.com/qbicsoftware/openbis20-scripts + A client software written in Java to query openBIS + jar + + 11 + UTF-8 + 5.3.31 + + + + + + true + always + fail + + + false + + maven-central + Maven central + https://repo.maven.apache.org/maven2 + + + + false + + + true + always + fail + + nexus-snapshots + QBiC Snapshots + https://qbic-repo.qbic.uni-tuebingen.de/repository/maven-snapshots + + + + true + always + fail + + + false + + nexus-releases + QBiC Releases + https://qbic-repo.qbic.uni-tuebingen.de/repository/maven-releases + + + + + true + nexus-releases + QBiC Releases + https://qbic-repo.qbic.uni-tuebingen.de/repository/maven-releases + + + false + nexus-snapshots + QBiC Snapshots + https://qbic-repo.qbic.uni-tuebingen.de/repository/maven-snapshots + + + + + + + org.apache.logging.log4j + log4j-core + 2.20.0 + + + org.slf4j + slf4j-api + 2.0.12 + + + org.slf4j + slf4j-simple + 2.0.12 + + + info.picocli + picocli + 4.6.2 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.17.2 + + + jline + jline + 2.14.6 + + + life.qbic + openbis-api + 20.10.7.3 + r1700646105 + + + life.qbic + openbis-api-core + 20.10.7.3 + r1700646105 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + + life.qbic.App + true + + + + jar-with-dependencies + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + + diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000..4657ac2 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: life.qbic.App + diff --git a/src/main/java/life/qbic/App.java b/src/main/java/life/qbic/App.java new file mode 100644 index 0000000..18ddaed --- /dev/null +++ b/src/main/java/life/qbic/App.java @@ -0,0 +1,105 @@ +package life.qbic; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import java.util.HashMap; +import java.util.Map; +import life.qbic.io.PropertyReader; +import life.qbic.io.commandline.CommandLineOptions; +import life.qbic.model.Configuration; +import life.qbic.model.download.AuthenticationException; +import life.qbic.model.download.ConnectionException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; + +import java.io.File; +import java.util.Arrays; + +/** + * Scripts to perform different openBIS queries + */ +public class App { + + private static final Logger LOG = LogManager.getLogger(App.class); + public static Map configProperties = new HashMap<>(); + + public static void main(String[] args) { + LOG.debug("command line arguments: " + Arrays.deepToString(args)); + CommandLine cmd = new CommandLine(new CommandLineOptions()); + int exitCode = cmd.execute(args); + System.exit(exitCode); + } + + public static void readConfig() { + System.err.println("reading config"); + String configPath = CommandLineOptions.getConfigPath(); + if(configPath != null && !configPath.isEmpty()) { + configProperties = PropertyReader.getProperties(configPath); + } + } + + /** + * checks if the commandline parameter for reading out the password from the environment variable + * is correctly provided + */ + private static Boolean isNotNullOrEmpty(String envVariableCommandLineParameter) { + return envVariableCommandLineParameter != null && !envVariableCommandLineParameter.isEmpty(); + } + + /** + * Logs into OpenBIS, asks for and verifies password. + * + * @return An instance of the Authentication class. + */ + public static OpenBIS loginToOpenBIS( + char[] password, String user, String url) { + setupLog(); + + OpenBIS authentication = new OpenBIS(url); + + return tryLogin(authentication, user, password); + } + + /** + * Logs into OpenBIS, asks for and verifies password, includes Datastore Server connection. + * + * @return An instance of the Authentication class. + */ + public static OpenBIS loginToOpenBIS( + char[] password, String user, String url, String dssUrl) { + setupLog(); + + int generousTimeOut = 30*60*1000; //30 mins + + OpenBIS authentication = new OpenBIS(url, dssUrl, generousTimeOut); + + return tryLogin(authentication, user, password); + } + + private static OpenBIS tryLogin(OpenBIS authentication, String user, char[] password) { + try { + authentication.login(user, new String(password)); + } catch (ConnectionException e) { + LOG.error(e.getMessage(), e); + LOG.error("Could not connect to QBiC's data source. Have you requested access to the " + + "server? If not please write to support@qbic.zendesk.com"); + System.exit(1); + } catch (AuthenticationException e) { + LOG.error(e.getMessage()); + System.exit(1); + } + return authentication; + } + + private static void setupLog() { + // Ensure 'logs' folder is created + File logFolder = new File(Configuration.LOG_PATH.toAbsolutePath().toString()); + if (!logFolder.exists()) { + boolean logFolderCreated = logFolder.mkdirs(); + if (!logFolderCreated) { + LOG.error("Could not create log folder '" + logFolder.getAbsolutePath() + "'"); + System.exit(1); + } + } + } +} 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..ce83c50 --- /dev/null +++ b/src/main/java/life/qbic/io/PetabParser.java @@ -0,0 +1,112 @@ +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 java.util.Set; +import life.qbic.model.petab.PetabMetadata; + +public class PetabParser { + + private final String META_INFO_YAML_NAME = "metaInformation"; + + 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 idKeyWord = "openBISId"; + + final String endOfLine = ":(.*)?(\\r\\n|[\\r\\n])"; + final String idInLine = idKeyWord+endOfLine; + + String content = Files.readString(path, charset); + // existing property found, fill/replace with relevant dataset code + if(content.contains(idKeyWord)) { + content = content.replaceAll(idInLine, idKeyWord+": "+datasetCode+"\n"); + // no existing property found, create it above the dateOfExperiment property + } else { + String dateKeyword = "dateOfExperiment"; + String dateLine = dateKeyword+endOfLine; + String newLines = idKeyWord+": "+datasetCode+"\n "+dateKeyword+":\n"; + content = content.replaceAll(dateLine, newLines); + } + Files.write(path, content.getBytes(charset)); + } + + private File findYaml(File directory) { + for (File file : Objects.requireNonNull(directory.listFiles())) { + String fileName = file.getName(); + if (file.isFile() && fileName.contains(META_INFO_YAML_NAME) && fileName.endsWith(".yaml")) { + return file; + } + if (file.isDirectory()) { + return findYaml(file); + } + } + System.out.println(META_INFO_YAML_NAME + " yaml not found."); + return null; + } + + public void addPatientIDs(String outputPath, Set patientIDs) { + System.err.println("found patient ids: "+patientIDs); + } +} diff --git a/src/main/java/life/qbic/io/PropertyReader.java b/src/main/java/life/qbic/io/PropertyReader.java new file mode 100644 index 0000000..61e89a0 --- /dev/null +++ b/src/main/java/life/qbic/io/PropertyReader.java @@ -0,0 +1,44 @@ +package life.qbic.io; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.TreeMap; + +public class PropertyReader { + + public static TreeMap getProperties(String infile) { + + TreeMap properties = new TreeMap<>(); + BufferedReader bfr = null; + try { + bfr = new BufferedReader(new FileReader(new File(infile))); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + + String line; + while (true) { + try { + if ((line = bfr.readLine()) == null) + break; + } catch (IOException e) { + throw new RuntimeException(e); + } + if (!line.startsWith("#") && !line.isEmpty()) { + String[] property = line.trim().split("="); + properties.put(property[0].trim(), property[1].trim()); + } + } + + try { + bfr.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return(properties); + } +} diff --git a/src/main/java/life/qbic/io/commandline/CommandLineOptions.java b/src/main/java/life/qbic/io/commandline/CommandLineOptions.java new file mode 100644 index 0000000..b44ea07 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/CommandLineOptions.java @@ -0,0 +1,41 @@ +package life.qbic.io.commandline; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +// main command with format specifiers for the usage help message +@Command(name = "openbis-scripts", + subcommands = {SampleHierarchyCommand.class, TransferSampleTypesToSeekCommand.class, + DownloadPetabCommand.class, UploadPetabResultCommand.class, UploadDatasetCommand.class, + SpaceStatisticsCommand.class, TransferDataToSeekCommand.class, FindDatasetsCommand.class, + CreateROCrate.class}, + description = "A client software for querying openBIS.", + mixinStandardHelpOptions = true, versionProvider = ManifestVersionProvider.class) +public class CommandLineOptions { + + private static final Logger LOG = LogManager.getLogger(CommandLineOptions.class); + + @Option(names = {"-config", "--config_file"}, + description = "Config file path to provide server and user information.", + scope = CommandLine.ScopeType.INHERIT) + static String configPath; + + @Option(names = {"-V", "--version"}, + versionHelp = true, + description = "print version information", + scope = CommandLine.ScopeType.INHERIT) + boolean versionRequested = false; + + @Option(names = {"-h", "--help"}, + usageHelp = true, + description = "display a help message and exit", + scope = CommandLine.ScopeType.INHERIT) + public boolean helpRequested = false; + + public static String getConfigPath() { + return configPath; + } +} diff --git a/src/main/java/life/qbic/io/commandline/CreateROCrate.java b/src/main/java/life/qbic/io/commandline/CreateROCrate.java new file mode 100644 index 0000000..b7f56f1 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/CreateROCrate.java @@ -0,0 +1,237 @@ +package life.qbic.io.commandline; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.ParserConfigurationException; +import life.qbic.App; +import life.qbic.model.DatasetWithProperties; +import life.qbic.model.OpenbisExperimentWithDescendants; +import life.qbic.model.OpenbisSeekTranslator; +import life.qbic.model.download.OpenbisConnector; +import life.qbic.model.isa.GenericSeekAsset; +import life.qbic.model.isa.ISAAssay; +import life.qbic.model.isa.ISASample; +import life.qbic.model.isa.NodeType; +import life.qbic.model.isa.SeekStructure; +import org.xml.sax.SAXException; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "ro-crate", + description = + "Transfers metadata and (optionally) data from openBIS to an RO-Crate-like structure that is " + + "based on assays, samples and one of several data types in SEEK). The data itself can " + + "be put into the crate using the '-d' flag. To completely exclude some dataset " + + "information from being transferred, a file ('--blacklist') containing dataset codes " + + "can be specified. The crate is not zipped at the moment.") +public class CreateROCrate implements Runnable { + + @Parameters(arity = "1", paramLabel = "openbis id", description = "The identifier of the " + + "experiment, sample or dataset to transfer.") + private String objectID; + @Parameters(arity = "1", paramLabel = "ro-path", description = "Path to the output folder") + private String roPath; + @Option(names = "--blacklist", description = "Path to file specifying by dataset " + + "dataset code which openBIS datasets not to transfer to SEEK. The file must contain one code " + + "per line.") + private String blacklistFile; + @Option(names = {"-d", "--data"}, description = + "Transfers the data itself to SEEK along with the metadata. " + + "Otherwise only the link(s) to the openBIS object will be created in SEEK.") + private boolean transferData; + @Mixin + OpenbisAuthenticationOptions openbisAuth = new OpenbisAuthenticationOptions(); + OpenbisConnector openbis; + OpenbisSeekTranslator translator; + + @Override + public void run() { + App.readConfig(); + System.out.printf("Transfer openBIS -> RO-crate started.%n"); + System.out.printf("Provided openBIS object: %s%n", objectID); + System.out.printf("Pack datasets into crate? %s%n", transferData); + if(blacklistFile!=null && !blacklistFile.isBlank()) { + System.out.printf("File with datasets codes that won't be transferred: %s%n", blacklistFile); + } + + System.out.println("Connecting to openBIS..."); + + OpenBIS authentication = App.loginToOpenBIS(openbisAuth.getOpenbisPassword(), + openbisAuth.getOpenbisUser(), openbisAuth.getOpenbisAS(), openbisAuth.getOpenbisDSS()); + + this.openbis = new OpenbisConnector(authentication); + + System.out.println("Searching for specified object in openBIS..."); + + boolean isExperiment = experimentExists(objectID); + NodeType nodeType = NodeType.ASSAY; + + if (!isExperiment && sampleExists(objectID)) { + nodeType = NodeType.SAMPLE; + } + + if (!isExperiment && !nodeType.equals(NodeType.SAMPLE) && datasetsExist( + Arrays.asList(objectID))) { + nodeType = NodeType.ASSET; + } + + if (nodeType.equals(NodeType.ASSAY) && !isExperiment) { + System.out.printf( + "%s could not be found in openBIS. Make sure you either specify an experiment, sample or dataset%n", + objectID); + return; + } + System.out.println("Search successful."); + + try { + translator = new OpenbisSeekTranslator(openbisAuth.getOpenbisBaseURL()); + } catch (IOException | ParserConfigurationException | SAXException e) { + throw new RuntimeException(e); + } + OpenbisExperimentWithDescendants structure; + System.out.println("Collecting information from openBIS..."); + switch (nodeType) { + case ASSAY: + structure = openbis.getExperimentWithDescendants(objectID); + break; + case SAMPLE: + structure = openbis.getExperimentAndDataFromSample(objectID); + break; + case ASSET: + structure = openbis.getExperimentStructureFromDataset(objectID); + break; + default: + throw new RuntimeException("Handling of node type " + nodeType + " is not supported."); + } + Set blacklist = parseBlackList(blacklistFile); + System.out.println("Translating openBIS structure to ISA structure..."); + try { + SeekStructure nodeWithChildren = translator.translateForRO(structure, blacklist, transferData); + String experimentID = nodeWithChildren.getAssayWithOpenBISReference().getRight(); + ISAAssay assay = nodeWithChildren.getAssayWithOpenBISReference().getLeft(); + String assayFileName = openbisIDToFileName(experimentID); + + String assayPath = Path.of(roPath, assayFileName).toString(); + new File(assayPath).mkdirs(); + + System.out.printf("Writing assay json for %s.%n", experimentID); + writeFile(Path.of(assayPath, assayFileName)+".json", assay.toJson()); + + for(ISASample sample : nodeWithChildren.getSamplesWithOpenBISReference().keySet()) { + String sampleID = nodeWithChildren.getSamplesWithOpenBISReference().get(sample); + String sampleFileName = openbisIDToFileName(sampleID); + String samplePath = Path.of(assayPath, sampleFileName).toString(); + new File(samplePath).mkdirs(); + + System.out.printf("Writing sample json for %s.%n", sampleID); + writeFile(Path.of(samplePath, sampleFileName)+".json", sample.toJson()); + } + + Map datasetIDToDataFolder = new HashMap<>(); + + for(DatasetWithProperties dwp : structure.getDatasets()) { + String sourceID = dwp.getClosestSourceID(); + String code = dwp.getCode(); + if(sourceID.equals(experimentID)) { + Path folderPath = Path.of(assayPath, code); + File dataFolder = new File(folderPath.toString()); + datasetIDToDataFolder.put(dwp.getCode(), dataFolder.getAbsolutePath()); + } else { + Path samplePath = Path.of(assayPath, openbisIDToFileName(sourceID), code); + File dataFolder = new File(samplePath.toString()); + datasetIDToDataFolder.put(dwp.getCode(), dataFolder.getAbsolutePath()); + } + } + + for(GenericSeekAsset asset : nodeWithChildren.getISAFileToDatasetFiles().keySet()) { + DataSetFile file = nodeWithChildren.getISAFileToDatasetFiles().get(asset); + String datasetID = file.getDataSetPermId().getPermId(); + String dataFolderPath = datasetIDToDataFolder.get(datasetID); + String assetJson = asset.toJson(); + String assetWithoutOriginFolder = asset.getFileName().replace("original",""); + File assetFolder = Path.of(dataFolderPath, assetWithoutOriginFolder).getParent().toFile(); + assetFolder.mkdirs(); + + String assetPath = Path.of(dataFolderPath, assetWithoutOriginFolder+".json").toString(); + System.out.printf("Writing asset json for file in dataset %s.%n", datasetID); + writeFile(assetPath, assetJson); + if(transferData) { + System.out.printf("Downloading dataset file to asset folder.%n"); + openbis.downloadDataset(dataFolderPath, datasetID, asset.getFileName()); + } + } + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + + System.out.println("Done"); + } + + private String openbisIDToFileName(String id) { + id = id.replace("/","_"); + if(id.startsWith("_")) { + return id.substring(1); + } else { + return id; + } + } + + private void writeFile(String path, String content) throws IOException { + FileWriter file = new FileWriter(path); + file.write(content); + file.close(); + } + + private Set parseBlackList(String blacklistFile) { + if(blacklistFile == null) { + return new HashSet<>(); + } + // trim whitespace, skip empty lines + try (Stream lines = Files.lines(Paths.get(blacklistFile)) + .map(String::trim) + .filter(s -> !s.isBlank())) { + + Set codes = lines.collect(Collectors.toSet()); + + for(String code : codes) { + if(!OpenbisConnector.datasetCodePattern.matcher(code).matches()) { + throw new RuntimeException("Invalid dataset code: " + code+". Please make sure to use valid" + + " dataset codes in the blacklist file."); + } + } + return codes; + } catch (IOException e) { + throw new RuntimeException(blacklistFile+" could not be found or read."); + } + } + + private boolean sampleExists(String objectID) { + return openbis.sampleExists(objectID); + } + + private boolean datasetsExist(List datasetCodes) { + return openbis.findDataSets(datasetCodes).size() == datasetCodes.size(); + } + + private boolean experimentExists(String experimentID) { + return openbis.experimentExists(experimentID); + } + +} 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..2b0f972 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/DownloadPetabCommand.java @@ -0,0 +1,77 @@ +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.Set; +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; + +/** + * The Download PEtab command can be used to download a PEtab Dataset from openBIS and store some + * additional information from openBIS in the metaInformation.yaml file (or a respective yaml file + * containing 'metaInformation' in its name). + * The Dataset to download is specified by providing the openBIS dataset identifier (code) and the + * PEtab is downloaded to the download path parameter provided. + * By design, the Dataset Identifier is added to the downloaded metaInformation.yaml as 'openBISId' + * in order to keep track of the source of this PEtab. + */ +@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 + OpenbisAuthenticationOptions auth = new OpenbisAuthenticationOptions(); + + @Override + public void run() { + App.readConfig(); + OpenBIS authentication = App.loginToOpenBIS(auth.getOpenbisPassword(), auth.getOpenbisUser(), + auth.getOpenbisAS(), auth.getOpenbisDSS()); + OpenbisConnector openbis = new OpenbisConnector(authentication); + + List datasets = openbis.findDataSets(Collections.singletonList(datasetCode)); + + if(datasets.isEmpty()) { + System.out.println("Dataset "+datasetCode+" not found"); + return; + } + DatasetWithProperties result = new DatasetWithProperties(datasets.get(0)); + Set patientIDs = openbis.findPropertiesInSampleHierarchy("PATIENT_DKFZ_ID", + result.getExperiment().getIdentifier()); + if(!patientIDs.isEmpty()) { + result.addProperty("patientIDs", String.join(",", patientIDs)); + } + + 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); + parser.addPatientIDs(outputPath, patientIDs); + } 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/FindDatasetsCommand.java b/src/main/java/life/qbic/io/commandline/FindDatasetsCommand.java new file mode 100644 index 0000000..d359a18 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/FindDatasetsCommand.java @@ -0,0 +1,116 @@ +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.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import life.qbic.App; +import life.qbic.model.DatasetWithProperties; +import life.qbic.model.download.OpenbisConnector; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * List Data + * The list-data command can be used to list Datasets in openBIS and some of their metadata based + * on the experiment or sample they are attached to. Experiments or samples are specified by their + * openBIS code or identifier. + * The optional 'space' parameter can be used to only show datasets found in the provided space. + */ +@Command(name = "list-data", + description = "lists datasets and their details for a given experiment code") +public class FindDatasetsCommand implements Runnable { + + @Parameters(arity = "1", paramLabel = "openBIS object", description = + "The code of the experiment or sample data is attached to.") + private String objectCode; + @Option(arity = "1", paramLabel = "", description = "Optional openBIS spaces to filter " + + "found datasets by.", names = {"-s", "--space"}) + private String space; + @Mixin + OpenbisAuthenticationOptions auth = new OpenbisAuthenticationOptions(); + + @Override + public void run() { + App.readConfig(); + 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..."); + } + if(objectCode.contains("/")) { + String[] splt = objectCode.split("/"); + objectCode = splt[splt.length-1]; + System.out.println("Query is not an object code, querying for: " + objectCode+" instead."); + } + OpenBIS authentication = App.loginToOpenBIS(auth.getOpenbisPassword(), auth.getOpenbisUser(), + auth.getOpenbisAS()); + OpenbisConnector openbis = new OpenbisConnector(authentication); + + List datasetsOfExp = openbis.listDatasetsOfExperiment(spaces, objectCode).stream() + .sorted(Comparator.comparing( + (DataSet d) -> d.getExperiment().getProject().getSpace().getCode())) + .collect(Collectors.toList()); + + List datasets = new ArrayList<>(); + + if(!datasetsOfExp.isEmpty()) { + System.out.printf("Found %s datasets for experiment %s:%n", datasetsOfExp.size(), objectCode); + datasets.addAll(datasetsOfExp); + } + List datasetsOfSample = openbis.listDatasetsOfSample(spaces, objectCode).stream() + .sorted(Comparator.comparing( + (DataSet d) -> d.getExperiment().getProject().getSpace().getCode())) + .collect(Collectors.toList()); + + if(!datasetsOfSample.isEmpty()) { + System.out.printf("Found %s datasets for sample %s:%n", datasetsOfSample.size(), objectCode); + + datasets.addAll(datasetsOfSample); + } + + Map properties = new HashMap<>(); + if (!datasets.isEmpty()) { + Set patientIDs = openbis.findPropertiesInSampleHierarchy("PATIENT_DKFZ_ID", + datasets.get(0).getExperiment().getIdentifier()); + if(!patientIDs.isEmpty()) { + properties.put("patientIDs", String.join(",", patientIDs)); + } + } + List datasetWithProperties = datasets.stream().map(dataSet -> { + DatasetWithProperties ds = new DatasetWithProperties(dataSet); + for (String key : properties.keySet()) { + ds.addProperty(key, properties.get(key)); + } + return ds; + }).collect(Collectors.toList()); + int datasetIndex = 0; + for (DatasetWithProperties dataSet : datasetWithProperties) { + datasetIndex++; + System.out.println("["+datasetIndex+"]"); + for(String key : dataSet.getProperties().keySet()) { + System.out.println(key+ ": "+properties.get(key)); + } + System.out.printf("ID: %s (%s)%n", dataSet.getCode(), dataSet.getExperiment().getIdentifier()); + System.out.println("Type: "+dataSet.getType().getCode()); + Person person = dataSet.getRegistrator(); + String simpleTime = new SimpleDateFormat("MM-dd-yy HH:mm:ss").format(dataSet.getRegistrationDate()); + String name = person.getFirstName() +" "+ person.getLastName(); + String uploadedBy = "Uploaded by "+name+" ("+simpleTime+")"; + System.out.println(uploadedBy); + System.out.println(); + } + } + +} diff --git a/src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java b/src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java new file mode 100644 index 0000000..afa48a3 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java @@ -0,0 +1,15 @@ +package life.qbic.io.commandline; + +import picocli.CommandLine; + +public class ManifestVersionProvider implements CommandLine.IVersionProvider { + @Override + public String[] getVersion() { + String implementationVersion = getClass().getPackage().getImplementationVersion(); + return new String[]{ + "version: " + implementationVersion, + "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", + "OS: ${os.name} ${os.version} ${os.arch}" + }; + } +} diff --git a/src/main/java/life/qbic/io/commandline/OpenbisAuthenticationOptions.java b/src/main/java/life/qbic/io/commandline/OpenbisAuthenticationOptions.java new file mode 100644 index 0000000..03a56f8 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/OpenbisAuthenticationOptions.java @@ -0,0 +1,132 @@ +package life.qbic.io.commandline; + +import static java.util.Objects.nonNull; +import static picocli.CommandLine.ArgGroup; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.StringJoiner; +import life.qbic.App; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; +import picocli.CommandLine.Option; + +public class OpenbisAuthenticationOptions { + private static final Logger log = LogManager.getLogger(OpenbisAuthenticationOptions.class); + + @Option( + names = {"-u", "--user"}, + description = "openBIS user name") + private String openbisUser; + @ArgGroup(multiplicity = "1") // ensures the password is provided once with at least one of the possible options. + OpenbisPasswordOptions openbisPasswordOptions; + + @Option( + names = {"-as", "-as_url"}, + description = "OpenBIS ApplicationServer URL", + scope = CommandLine.ScopeType.INHERIT) + private String as_url; + + @Option( + names = {"-dss", "--dss_url"}, + description = "OpenBIS DatastoreServer URL", + scope = CommandLine.ScopeType.INHERIT) + private String dss_url; + + public String getOpenbisUser() { + if(openbisUser == null && App.configProperties.containsKey("user")) { + openbisUser = App.configProperties.get("user"); + } + if(openbisUser == null) { + log.error("No openBIS user provided."); + System.exit(2); + } + return openbisUser; + } + + public String getOpenbisDSS() { + if(dss_url == null && App.configProperties.containsKey("dss")) { + dss_url = App.configProperties.get("dss"); + } + if(dss_url == null) { + log.error("No openBIS datastore server URL provided."); + System.exit(2); + } + return dss_url; + } + + public String getOpenbisAS() { + if(as_url == null && App.configProperties.containsKey("as")) { + as_url = App.configProperties.get("as"); + } + if(as_url == null) { + log.error("No openBIS application server URL provided."); + System.exit(2); + } + return as_url; + } + + public char[] getOpenbisPassword() { + return openbisPasswordOptions.getPassword(); + } + + public String getOpenbisBaseURL() throws MalformedURLException { + URL asURL = new URL(as_url); + String res = asURL.getProtocol()+ "://" +asURL.getHost(); + if(asURL.getPort()!=-1) { + res+=":"+asURL.getPort(); + } + return res; + } + + /** + * official picocli documentation example + */ + static class OpenbisPasswordOptions { + @Option(names = "--openbis-pw:env", arity = "1", paramLabel = "", + description = "provide the name of an environment variable to read in your password from") + protected String passwordEnvironmentVariable = ""; + + @Option(names = "--openbis-pw:prop", arity = "1", paramLabel = "", + description = "provide the name of a system property to read in your password from") + protected String passwordProperty = ""; + + @Option(names = "--openbis-pw", arity = "0", + description = "please provide your openBIS password", interactive = true) + protected char[] password = null; + + /** + * Gets the password. If no password is provided, the program exits. + * @return the password provided by the user. + */ + char[] getPassword() { + if (nonNull(password)) { + return password; + } + // System.getProperty(String key) does not work for empty or blank keys. + if (!passwordProperty.isBlank()) { + String systemProperty = System.getProperty(passwordProperty); + if (nonNull(systemProperty)) { + return systemProperty.toCharArray(); + } + } + String environmentVariable = System.getenv(passwordEnvironmentVariable); + if (nonNull(environmentVariable) && !environmentVariable.isBlank()) { + return environmentVariable.toCharArray(); + } + log.error("No password provided. Please provide your password."); + System.exit(2); + return null; // not reachable due to System.exit in previous line + } + } + + @Override + public String toString() { + return new StringJoiner(", ", OpenbisAuthenticationOptions.class.getSimpleName() + "[", "]") + .add("user='" + openbisUser + "'") + .toString(); + //ATTENTION: do not expose the password here! + } + +} \ No newline at end of file diff --git a/src/main/java/life/qbic/io/commandline/SampleHierarchyCommand.java b/src/main/java/life/qbic/io/commandline/SampleHierarchyCommand.java new file mode 100644 index 0000000..04fd913 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/SampleHierarchyCommand.java @@ -0,0 +1,86 @@ +package life.qbic.io.commandline; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import life.qbic.App; +import life.qbic.model.Configuration; +import life.qbic.model.SampleTypeConnection; +import life.qbic.model.download.FileSystemWriter; +import life.qbic.model.download.SummaryWriter; +import life.qbic.model.download.OpenbisConnector; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * The Sample Types command queries all sample types and prints which types are connected and how + * often (via samples existing in the queried openBIS instance), creating a sample type hierarchy. + * The --space command can be used to only show the sample-types used in a specific openBIS space. + * An output file for the resulting hierarchy can be specified using the --out command. + */ +@Command(name = "sample-types", + description = "lists sample types with children sample types and how often they are found in " + + "the openbis instance") +public class SampleHierarchyCommand implements Runnable { + + @Option(arity = "1", paramLabel = "", description = "optional openBIS space to filter " + + "samples", names = {"-s", "--space"}) + private String space; + @Option(arity = "1", paramLabel = "", description = "optional output path", + names = {"-o", "--out"}) + private String outpath; + @Mixin + OpenbisAuthenticationOptions auth = new OpenbisAuthenticationOptions(); + + @Override + public void run() { + App.readConfig(); + List summary = new ArrayList<>(); + List spaces = new ArrayList<>(); + if(space!=null) { + summary.add("Querying samples in space: "+space+"..."); + spaces.add(space); + } else { + summary.add("Querying samples in all available spaces..."); + } + OpenBIS authentication = App.loginToOpenBIS(auth.getOpenbisPassword(), + auth.getOpenbisUser(), auth.getOpenbisAS()); + OpenbisConnector openbis = new OpenbisConnector(authentication); + Map hierarchy = openbis.queryFullSampleHierarchy(spaces); + + hierarchy.entrySet().stream() + .sorted(Entry.comparingByValue()) + .forEach(entry -> summary.add(entry.getKey()+" ("+entry.getValue()+")")); + + for(String s : summary) { + System.out.println(s); + } + Path outputPath = Paths.get(Configuration.LOG_PATH.toString(), + "sample_model_summary" + getTimeStamp() + ".txt"); + if(outpath!=null) { + outputPath = Paths.get(outpath); + } + SummaryWriter summaryWriter = new FileSystemWriter(outputPath); + try { + summaryWriter.reportSummary(summary); + } catch (IOException e) { + throw new RuntimeException("Could not write summary file."); + } + } + + 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/SeekAuthenticationOptions.java b/src/main/java/life/qbic/io/commandline/SeekAuthenticationOptions.java new file mode 100644 index 0000000..545949f --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/SeekAuthenticationOptions.java @@ -0,0 +1,98 @@ +package life.qbic.io.commandline; + +import static java.util.Objects.nonNull; +import static picocli.CommandLine.ArgGroup; + +import java.util.StringJoiner; +import life.qbic.App; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; +import picocli.CommandLine.Option; + +public class SeekAuthenticationOptions { + private static final Logger log = LogManager.getLogger(SeekAuthenticationOptions.class); + + @ArgGroup(multiplicity = "1") + SeekPasswordOptions seekPasswordOptions; + + @Option( + names = {"-su", "--seek-user"}, + description = "Seek user name (email)", + scope = CommandLine.ScopeType.INHERIT) + private String seekUser; + @Option( + names = {"-seek-server", "-seek_url"}, + description = "SEEK API URL", + scope = CommandLine.ScopeType.INHERIT) + private String seek_url; + + public String getSeekUser() { + if(seekUser == null && App.configProperties.containsKey("seek_user")) { + seekUser = App.configProperties.get("seek_user"); + } + if (seekUser == null) { + log.error("No SEEK user/email provided."); + System.exit(2); + } + return seekUser; + } + + public String getSeekURL() { + if(seek_url != null || App.configProperties.containsKey("seek_url")) { + seek_url = App.configProperties.get("seek_url"); + } + if(seek_url == null) { + log.error("No URL to the SEEK address provided."); + System.exit(2); + } + return seek_url; + } + + public char[] getSeekPassword() { + return seekPasswordOptions.getPassword(); + } + + static class SeekPasswordOptions { + @Option(names = "--seek-pw:env", arity = "1", paramLabel = "", description = "provide the name of an environment variable to read in your password from") + protected String passwordEnvironmentVariable = ""; + + @Option(names = "--seek-pw:prop", arity = "1", paramLabel = "", description = "provide the name of a system property to read in your password from") + protected String passwordProperty = ""; + + @Option(names = "--seek-pw", arity = "0", description = "please provide your SEEK password", interactive = true) + protected char[] password = null; + + /** + * Gets the password. If no password is provided, the program exits. + * @return the password provided by the user. + */ + char[] getPassword() { + if (nonNull(password)) { + return password; + } + // System.getProperty(String key) does not work for empty or blank keys. + if (!passwordProperty.isBlank()) { + String systemProperty = System.getProperty(passwordProperty); + if (nonNull(systemProperty)) { + return systemProperty.toCharArray(); + } + } + String environmentVariable = System.getenv(passwordEnvironmentVariable); + if (nonNull(environmentVariable) && !environmentVariable.isBlank()) { + return environmentVariable.toCharArray(); + } + log.error("No password provided. Please provide your password."); + System.exit(2); + return null; // not reachable due to System.exit in previous line + } + + } + @Override + public String toString() { + return new StringJoiner(", ", SeekAuthenticationOptions.class.getSimpleName() + "[", "]") + .add("user='" + seekUser + "'") + .toString(); + //ATTENTION: do not expose the password here! + } +} \ No newline at end of file diff --git a/src/main/java/life/qbic/io/commandline/SpaceStatisticsCommand.java b/src/main/java/life/qbic/io/commandline/SpaceStatisticsCommand.java new file mode 100644 index 0000000..e4cb97e --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/SpaceStatisticsCommand.java @@ -0,0 +1,147 @@ +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.experiment.Experiment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import life.qbic.App; +import life.qbic.model.Configuration; +import life.qbic.model.download.FileSystemWriter; +import life.qbic.model.download.SummaryWriter; +import life.qbic.model.download.OpenbisConnector; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * The Statistics command can be used to list the number of collections, sample objects and attached + * datasets by type for one or all spaces accessible by the user. + * The --space command can be used to only show the objects in a specific openBIS space. + * An output file for the resulting list can be specified using the --out command. + * By default, openBIS settings objects and material spaces are ignored. This can be overwritten + * using --show-settings. + */ +@Command(name = "statistics", + description = "lists the number of collections, sample objects and attached datasets (by type)" + + "for one or all spaces accessible by the user") +public class SpaceStatisticsCommand implements Runnable { + + @Option(arity = "1", paramLabel = "", description = "optional openBIS space to filter " + + "samples", names = {"-s", "--space"}) + private String space; + @Option(arity = "1", paramLabel = "", description = "optional output path", + names = {"-o", "--out"}) + private String outpath; + @Option(arity = "0", description = "shows results for openBIS settings and material spaces. " + + "Ignored if a specific space is selected.", + names = {"--show-settings"}) + private boolean allSpaces; + @Mixin + OpenbisAuthenticationOptions auth = new OpenbisAuthenticationOptions(); + + @Override + public void run() { + App.readConfig(); + List summary = new ArrayList<>(); + List blackList = new ArrayList<>(Arrays.asList("ELN_SETTINGS", "MATERIAL.GLOBAL")); + List spaces = new ArrayList<>(); + if (space != null) { + summary.add("Querying samples in space: " + space); + spaces.add(space); + } else { + summary.add("Querying samples in all available spaces..."); + } + OpenBIS authentication = App.loginToOpenBIS(auth.getOpenbisPassword(), auth.getOpenbisUser(), auth.getOpenbisAS()); + OpenbisConnector openbis = new OpenbisConnector(authentication); + + if (spaces.isEmpty()) { + spaces = openbis.getSpaces(); + if(!allSpaces) { + spaces.removeAll(blackList); + } + } + + Map>> experiments = openbis.getExperimentsByTypeAndSpace(spaces); + Map>> samples = openbis.getSamplesByTypeAndSpace(spaces); + Map>> datasets = openbis.getDatasetsByTypeAndSpace(spaces); + + for(String space : spaces) { + summary.add("-----"); + summary.add("Summary for "+space); + summary.add("-----"); + int numExps = 0; + if (experiments.containsKey(space)) { + numExps = experiments.get(space).values().stream().mapToInt(List::size).sum(); + } + summary.add("Experiments ("+numExps+"):"); + summary.add(""); + if(!experiments.isEmpty()) { + Map> exps = experiments.get(space); + for (String type : exps.keySet()) { + summary.add(type + ": " + exps.get(type).size()); + } + } + summary.add(""); + int numSamples = 0; + if (samples.containsKey(space)) { + numSamples = samples.get(space).values().stream().mapToInt(List::size).sum(); + } + summary.add("Samples ("+numSamples+"):"); + summary.add(""); + if(!samples.isEmpty()) { + Map> samps = samples.get(space); + for (String type : samps.keySet()) { + summary.add(type + ": " + samps.get(type).size()); + } + } + summary.add(""); + int numData = 0; + if (datasets.containsKey(space)) { + numData = datasets.get(space).values().stream().mapToInt(List::size).sum(); + } + summary.add("Attached datasets (" + numData + "):"); + summary.add(""); + if (datasets.get(space) != null) { + Map> dsets = datasets.get(space); + for (String dataType : dsets.keySet()) { + summary.add(dataType + ": " + dsets.get(dataType).size()); + } + } + + summary.add(""); + } + + for(String line : summary) { + System.out.println(line); + } + + Path outputPath = Paths.get(Configuration.LOG_PATH.toString(), + "spaces_summary_"+getTimeStamp()+".txt"); + if(outpath!=null) { + outputPath = Paths.get(outpath); + } + SummaryWriter summaryWriter = new FileSystemWriter(outputPath); + try { + summaryWriter.reportSummary(summary); + } catch (IOException e) { + throw new RuntimeException("Could not write summary file."); + } + } + + 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).toString(); + } +} diff --git a/src/main/java/life/qbic/io/commandline/TransferDataToSeekCommand.java b/src/main/java/life/qbic/io/commandline/TransferDataToSeekCommand.java new file mode 100644 index 0000000..5fbd964 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/TransferDataToSeekCommand.java @@ -0,0 +1,419 @@ +package life.qbic.io.commandline; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.ParserConfigurationException; +import life.qbic.App; +import life.qbic.model.OpenbisExperimentWithDescendants; +import life.qbic.model.OpenbisSeekTranslator; +import life.qbic.model.download.SEEKConnector.SeekStructurePostRegistrationInformation; +import life.qbic.model.isa.NodeType; +import life.qbic.model.isa.SeekStructure; +import life.qbic.model.download.OpenbisConnector; +import life.qbic.model.download.SEEKConnector; +import life.qbic.model.download.SEEKConnector.AssetToUpload; +import org.apache.commons.codec.binary.Base64; +import org.xml.sax.SAXException; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "openbis-to-seek", + description = + "Transfers metadata and (optionally) data from openBIS to SEEK. Experiments, samples and " + + "dataset information are always transferred together (as assays, samples and one of " + + "several data types in SEEK). Dataset info always links back to the openBIS path of " + + "the respective dataset. The data itself can be transferred and stored in SEEK " + + "using the '-d' flag." + + "To completely exclude some dataset information from being transferred, a " + + "file ('--blacklist') containing dataset codes (from openBIS) can be specified." + + "Unless otherwise specified ('--no-update' flag), the command will try to update " + + "existing nodes in SEEK (recognized by openBIS identifiers in their metadata).") +public class TransferDataToSeekCommand implements Runnable { + + @Parameters(arity = "1", paramLabel = "openbis id", description = "The identifier of the " + + "experiment, sample or dataset to transfer.") + private String objectID; + @Parameters(arity = "1", paramLabel = "seek-study", description = "Title of the study in SEEK where " + + "nodes should be added. Mandatory, as an assay is always needed and attached to a study.") + private String studyTitle; + @Option(names = "--seek-project", description = "Title of the project in SEEK where nodes should" + + "be added. Can alternatively be provided via the config file as 'seek_default_project'.") + private String projectTitle; + @Option(names = "--blacklist", description = "Path to file specifying by dataset " + + "dataset code which openBIS datasets not to transfer to SEEK. The file must contain one code " + + "per line.") + private String blacklistFile; + @Option(names = "--no-update", description = "Use to specify that existing " + + "information in SEEK for the specified openBIS input should not be updated, but new nodes " + + "created.") + private boolean noUpdate; + @Option(names = {"-d", "--data"}, description = + "Transfers the data itself to SEEK along with the metadata. " + + "Otherwise only the link(s) to the openBIS object will be created in SEEK.") + private boolean transferData; + @Mixin + SeekAuthenticationOptions seekAuth = new SeekAuthenticationOptions(); + @Mixin + OpenbisAuthenticationOptions openbisAuth = new OpenbisAuthenticationOptions(); + OpenbisConnector openbis; + SEEKConnector seek; + OpenbisSeekTranslator translator; + //500 MB - user will be informed that the transfer will take a while, for each file larger than this + private final long FILE_WARNING_SIZE = 500*1024*1024; + + @Override + public void run() { + App.readConfig(); + System.out.printf("Transfer openBIS -> SEEK started.%n"); + System.out.printf("Provided openBIS object: %s%n", objectID); + System.out.printf("Provided SEEK study title: %s%n", studyTitle); + if(projectTitle!=null && !projectTitle.isBlank()) { + System.out.printf("Provided SEEK project title: %s%n", projectTitle); + } else { + System.out.printf("No SEEK project title provided, will search config file.%n"); + } + System.out.printf("Transfer datasets to SEEK? %s%n", transferData); + System.out.printf("Update existing nodes if found? %s%n", !noUpdate); + if(blacklistFile!=null && !blacklistFile.isBlank()) { + System.out.printf("File with datasets codes that won't be transferred: %s%n", blacklistFile); + } + + System.out.println("Connecting to openBIS..."); + + OpenBIS authentication = App.loginToOpenBIS(openbisAuth.getOpenbisPassword(), + openbisAuth.getOpenbisUser(), openbisAuth.getOpenbisAS(), openbisAuth.getOpenbisDSS()); + + this.openbis = new OpenbisConnector(authentication); + + System.out.println("Searching for specified object in openBIS..."); + + boolean isExperiment = experimentExists(objectID); + NodeType nodeType = NodeType.ASSAY; + + if (!isExperiment && sampleExists(objectID)) { + nodeType = NodeType.SAMPLE; + } + + if (!isExperiment && !nodeType.equals(NodeType.SAMPLE) && datasetsExist(Arrays.asList(objectID))) { + nodeType = NodeType.ASSET; + } + + if (nodeType.equals(NodeType.ASSAY) && !isExperiment) { + System.out.printf( + "%s could not be found in openBIS. Make sure you either specify an experiment, sample or dataset%n", + objectID); + return; + } + System.out.println("Search successful."); + System.out.println("Connecting to SEEK..."); + + byte[] httpCredentials = Base64.encodeBase64( + (seekAuth.getSeekUser() + ":" + new String(seekAuth.getSeekPassword())).getBytes()); + try { + String project = App.configProperties.get("seek_default_project"); + if(project == null || project.isBlank()) { + throw new RuntimeException("a default project must be provided via config "+ + "('seek_default_project') or parameter."); + } + seek = new SEEKConnector(seekAuth.getSeekURL(), httpCredentials, + openbisAuth.getOpenbisBaseURL(), App.configProperties.get("seek_default_project")); + seek.setDefaultStudy(studyTitle); + translator = seek.getTranslator(); + } catch (URISyntaxException | IOException | InterruptedException | + ParserConfigurationException | SAXException e) { + throw new RuntimeException(e); + } + SeekStructurePostRegistrationInformation postRegInfo; + OpenbisExperimentWithDescendants structure; + try { + System.out.println("Collecting information from openBIS..."); + switch (nodeType) { + case ASSAY: + structure = openbis.getExperimentWithDescendants(objectID); + break; + case SAMPLE: + structure = openbis.getExperimentAndDataFromSample(objectID); + break; + case ASSET: + structure = openbis.getExperimentStructureFromDataset(objectID); + break; + default: + throw new RuntimeException("Handling of node type " + nodeType + " is not supported."); + } + postRegInfo = handleExperimentTransfer(structure, nodeType); + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new RuntimeException(e); + } + + System.out.println("Creating links to new SEEK objects in openBIS..."); + openbis.createSeekLinks(postRegInfo); + + System.out.println("Done"); + } + + private SeekStructurePostRegistrationInformation handleExperimentTransfer( + OpenbisExperimentWithDescendants experiment, NodeType nodeType) + throws URISyntaxException, IOException, InterruptedException { + Set blacklist = parseBlackList(blacklistFile); + System.out.println("Translating openBIS property codes to SEEK names..."); + Map sampleTypesToIds = seek.getSampleTypeNamesToIDs(); + System.out.println("Creating SEEK structure..."); + SeekStructure nodeWithChildren = translator.translate(experiment, sampleTypesToIds, blacklist, + transferData); + if (!noUpdate) { + System.out.println("Trying to find existing corresponding assay in SEEK..."); + Optional assayID = getAssayIDForOpenBISExperiment(experiment.getExperiment()); + assayID.ifPresent(x -> System.out.println("Found assay with id " + assayID.get())); + if (assayID.isEmpty()) { + System.out.println("Creating new node(s)..."); + return createNewAssayStructure(nodeWithChildren); + } else { + System.out.println("Updating nodes..."); + return updateAssayStructure(nodeWithChildren, assayID.get()); + } + } + System.out.println("Creating new node(s)..."); + return createNewAssayStructure(nodeWithChildren); + } + + /* + private SeekStructurePostRegistrationInformation handleSampleTransfer() + throws URISyntaxException, IOException, InterruptedException { + System.out.println("Collecting information from openBIS..."); + Set blacklist = parseBlackList(blacklistFile); + System.out.println("Translating openBIS property codes to SEEK names..."); + Map sampleTypesToIds = seek.getSampleTypeNamesToIDs(); + System.out.println("Trying to find existing corresponding sample in SEEK..."); + Optional sampleID = getSampleIDForOpenBISSample(sampleWithDatasets.getSample()); + sampleID.ifPresent(x -> System.out.println("Found sample with id "+sampleID.get())); + SeekStructure nodeWithChildren = translator.translate(sampleWithDatasets, sampleTypesToIds, + blacklist, transferData); + + System.out.println("Creating SEEK structure..."); + if(sampleID.isEmpty() || noUpdate) { + System.out.println("Creating new node(s)..."); + return createNewSampleStructure(nodeWithChildren); + } else { + System.out.println("Updating nodes..."); + return updateSampleStructure(nodeWithChildren, sampleID.get()); + } + } + + private SeekStructurePostRegistrationInformation handleDatasetTransfer() + throws URISyntaxException, IOException, InterruptedException { + System.out.println("Collecting information from openBIS..."); + Pair> datasetWithFiles = openbis.getDataSetWithFiles( + objectID); + Set blacklist = parseBlackList(blacklistFile); + //TODO is this necessary? + //System.out.println("Trying to find existing corresponding assets in SEEK..."); + //List assetIDs = seek.searchAssetsContainingKeyword(datasetWithFiles.getLeft().getCode()); + //System.out.println("Found existing asset ids: "+assetIDs); + SeekStructure nodeWithChildren = translator.translate(datasetWithFiles, blacklist, transferData); + + System.out.println("Creating new asset(s)..."); + return createNewAssetsForDataset(nodeWithChildren.getISAFileToDatasetFiles()); + } + + + private SeekStructurePostRegistrationInformation updateSampleStructure( + SeekStructure nodeWithChildren, String sampleID) + throws URISyntaxException, IOException, InterruptedException { + SeekStructurePostRegistrationInformation postRegInfo = + seek.updateSampleNode(nodeWithChildren, sampleID); + List assetsToUpload = postRegInfo.getAssetsToUpload(); + if (transferData) { + handleDataTransfer(assetsToUpload); + } + postRegInfo.getExperimentIDWithEndpoint().ifPresentOrElse( + (value) -> System.out.printf("%s was successfully updated.%n", value.getRight()), + () -> System.out.printf("Update performed, but assay id not found in post update info.%n") + ); + return postRegInfo; + } + + private SeekStructurePostRegistrationInformation createNewSampleStructure( + SeekStructure nodeWithChildren) throws URISyntaxException, IOException, InterruptedException { + SeekStructurePostRegistrationInformation postRegistrationInformation = + seek.createSampleWithAssets(nodeWithChildren); + List assetsOfAssayToUpload = postRegistrationInformation.getAssetsToUpload(); + if (transferData) { + handleDataTransfer(assetsOfAssayToUpload); + } + System.out.printf("Sample was successfully created.%n"); + return postRegistrationInformation; + } + + private SeekStructurePostRegistrationInformation createNewAssetsForDataset( + Map assets) throws URISyntaxException, IOException, + InterruptedException { + + SeekStructurePostRegistrationInformation postRegistrationInformation = + seek.createStandaloneAssets(assets); + List assetsOfAssayToUpload = postRegistrationInformation.getAssetsToUpload(); + if (transferData) { + handleDataTransfer(assetsOfAssayToUpload); + } + System.out.printf("Assets were successfully created.%n"); + return postRegistrationInformation; + } + + */ + + private Set parseBlackList(String blacklistFile) { + if(blacklistFile == null) { + return new HashSet<>(); + } + // trim whitespace, skip empty lines + try (Stream lines = Files.lines(Paths.get(blacklistFile)) + .map(String::trim) + .filter(s -> !s.isBlank())) { + + Set codes = lines.collect(Collectors.toSet()); + + for(String code : codes) { + if(!OpenbisConnector.datasetCodePattern.matcher(code).matches()) { + throw new RuntimeException("Invalid dataset code: " + code+". Please make sure to use valid" + + " dataset codes in the blacklist file."); + } + } + return codes; + } catch (IOException e) { + throw new RuntimeException(blacklistFile+" could not be found or read."); + } + } + + private SeekStructurePostRegistrationInformation updateAssayStructure( + SeekStructure nodeWithChildren, String assayID) throws URISyntaxException, + IOException, InterruptedException { + SeekStructurePostRegistrationInformation postRegInfo = seek.updateAssayNode(nodeWithChildren, + assayID); + List assetsToUpload = postRegInfo.getAssetsToUpload(); + if (transferData) { + handleDataTransfer(assetsToUpload); + } + postRegInfo.getExperimentIDWithEndpoint().ifPresentOrElse( + (value) -> System.out.printf("%s was successfully updated.%n", value.getRight()), + () -> System.out.printf("Update performed, but assay id not found in post update info.%n") + ); + return postRegInfo; + } + + private SeekStructurePostRegistrationInformation createNewAssayStructure( + SeekStructure nodeWithChildren) + throws URISyntaxException, IOException, InterruptedException { + SeekStructurePostRegistrationInformation postRegInfo = + seek.createNode(nodeWithChildren); + List assetsToUpload = postRegInfo.getAssetsToUpload(); + if (transferData) { + handleDataTransfer(assetsToUpload); + } + System.out.printf("Assay was successfully created.%n"); + return postRegInfo; + } + + private void handleDataTransfer(List assets) + throws URISyntaxException, IOException, InterruptedException { + final String tmpFolderPath = "tmp/"; + for(AssetToUpload asset : assets) { + String filePath = asset.getFilePath(); + String dsCode = asset.getDataSetCode(); + if(asset.getFileSizeInBytes() > 1000*1024*1024) { + System.out.printf("Skipping %s due to size...%n", + filePath); + } else if (asset.getFileSizeInBytes() > 300 * 1024 * 1024) { + System.out.printf("File is %s MB...streaming might take a while%n", + asset.getFileSizeInBytes() / (1024 * 1024)); + System.out.printf("Downloading file %s from openBIS to tmp folder due to size...%n", + filePath); + File tmpFile = openbis.downloadDataset(tmpFolderPath, dsCode, filePath); + + System.out.printf("Uploading file to SEEK...%n"); + String fileURL = seek.uploadFileContent(asset.getBlobEndpoint(), tmpFile.getAbsolutePath()); + System.out.printf("File stored here: %s%n", fileURL); + } else { + System.out.printf("Streaming file %s from openBIS to SEEK...%n", asset.getFilePath()); + + String fileURL = seek.uploadStreamContent(asset.getBlobEndpoint(), + () -> openbis.streamDataset(asset.getDataSetCode(), asset.getFilePath())); + System.out.printf("File stored here: %s%n", fileURL); + + } + System.out.printf("Cleaning up temp folder%n"); + cleanupTemp(new File(tmpFolderPath)); + } + } + + private boolean sampleExists(String objectID) { + return openbis.sampleExists(objectID); + } + + private boolean datasetsExist(List datasetCodes) { + return openbis.findDataSets(datasetCodes).size() == datasetCodes.size(); + } + + private boolean experimentExists(String experimentID) { + return openbis.experimentExists(experimentID); + } + + private Optional getAssayIDForOpenBISExperiment(Experiment experiment) + throws URISyntaxException, IOException, InterruptedException { + // the perm id is unique and afaik not used by scientists. it is highly unlikely that it would + // "accidentally" be part of another title or description. however, care should be taken here, + // because if a perm id is found in the wrong SEEK node, meta-information in SEEK could be + // overwritten or samples/data added to the wrong assay. + String permID = experiment.getPermId().getPermId(); + List assayIDs = seek.searchAssaysInStudyContainingKeyword(permID); + if(assayIDs.isEmpty()) { + return Optional.empty(); + } + if(assayIDs.size() == 1) { + return Optional.of(assayIDs.get(0)); + } + throw new RuntimeException("Experiment identifier "+permID+ " was found in more than one assay: " + +assayIDs+". Don't know which assay to update."); + } + + private Optional getSampleIDForOpenBISSample(Sample sample) + throws URISyntaxException, IOException, InterruptedException { + String id = sample.getIdentifier().getIdentifier(); + List sampleIDs = seek.searchSamplesContainingKeyword(id); + if(sampleIDs.isEmpty()) { + return Optional.empty(); + } + if(sampleIDs.size() == 1) { + return Optional.of(sampleIDs.get(0)); + } + throw new RuntimeException("Experiment identifier "+id+ " was found in more than one sample: "+sampleIDs); + } + + private void cleanupTemp(File tmpFolder) { + File[] files = tmpFolder.listFiles(); + if (files != null) { //some JVMs return null for empty dirs + for (File f : files) { + if (f.isDirectory()) { + cleanupTemp(f); + } else { + f.delete(); + } + } + } + } + +} diff --git a/src/main/java/life/qbic/io/commandline/TransferSampleTypesToSeekCommand.java b/src/main/java/life/qbic/io/commandline/TransferSampleTypesToSeekCommand.java new file mode 100644 index 0000000..53eecd1 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/TransferSampleTypesToSeekCommand.java @@ -0,0 +1,83 @@ +package life.qbic.io.commandline; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType; +import java.io.IOException; +import java.net.URISyntaxException; +import javax.xml.parsers.ParserConfigurationException; +import life.qbic.App; +import life.qbic.model.OpenbisSeekTranslator; +import life.qbic.model.SampleTypesAndMaterials; +import life.qbic.model.download.OpenbisConnector; +import life.qbic.model.download.SEEKConnector; +import org.apache.commons.codec.binary.Base64; +import org.xml.sax.SAXException; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = "sample-type-transfer", + description = + "Transfers sample types from openBIS to SEEK.") +public class TransferSampleTypesToSeekCommand implements Runnable { + @Mixin + SeekAuthenticationOptions seekAuth = new SeekAuthenticationOptions(); + @Mixin + OpenbisAuthenticationOptions openbisAuth = new OpenbisAuthenticationOptions(); + @Option(names = "--ignore-existing", description = "Use to specify that existing " + + "sample-types of the same name in SEEK should be ignored and the sample-type created a " + + "second time.") + private boolean ignoreExisting; + OpenbisConnector openbis; + SEEKConnector seek; + OpenbisSeekTranslator translator; + + @Override + public void run() { + App.readConfig(); + + System.out.println("auth..."); + + OpenBIS authentication = App.loginToOpenBIS(openbisAuth.getOpenbisPassword(), + openbisAuth.getOpenbisUser(), openbisAuth.getOpenbisAS(), openbisAuth.getOpenbisDSS()); + System.out.println("openbis..."); + + openbis = new OpenbisConnector(authentication); + + byte[] httpCredentials = Base64.encodeBase64( + (seekAuth.getSeekUser() + ":" + new String(seekAuth.getSeekPassword())).getBytes()); + try { + String project = App.configProperties.get("seek_default_project"); + if(project == null || project.isBlank()) { + throw new RuntimeException("a default project must be provided via config "+ + "('seek_default_project') or parameter."); + } + seek = new SEEKConnector(seekAuth.getSeekURL(), httpCredentials, openbisAuth.getOpenbisBaseURL(), + App.configProperties.get("seek_default_project")); + translator = seek.getTranslator(); + } catch (URISyntaxException | IOException | InterruptedException | + ParserConfigurationException | SAXException e) { + throw new RuntimeException(e); + } + + SampleTypesAndMaterials types = openbis.getSampleTypesWithMaterials(); + + try { + for(SampleType type : types.getSampleTypes()) { + System.err.println("creating "+type.getCode()); + if(!ignoreExisting && seek.sampleTypeExists(type.getCode())) { + System.err.println(type.getCode()+ " is already in SEEK. If you want to create a new " + + "sample type using the same name, you can use the --ignore-existing flag."); + } else { + String sampleTypeId = seek.createSampleType(translator.translate(type)); + System.err.println("created "+sampleTypeId); + } + } + } catch (URISyntaxException | IOException | InterruptedException e) { + 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 new file mode 100644 index 0000000..dc4532c --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/UploadDatasetCommand.java @@ -0,0 +1,96 @@ +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.model.download.OpenbisConnector; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * The Upload Dataset command can be used to upload a Dataset to openBIS and connect it to existing + * datasets. + * To upload a dataset, the path to the file or folder and the object ID to which it should + * be attached need to be provided. Objects can be experiments or samples. + * Parent datasets can be specified using the --parents command. + * If the specified object ID or any of the specified parent datasets cannot be found, the script + * will stop and return an error message. + * The dataset type of the new dataset in openBIS can be specified using the --type option, + * otherwise the type "UNKNOWN" will be used. + */ +@Command(name = "upload-data", + description = "uploads a dataset and attaches it to an experiment or sample and (optionally) " + + "other datasets") +public class UploadDatasetCommand 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 = "object ID", description = "The full identifier of the " + + "experiment or sample the data should be attached to. The identifier must be of the format: " + + "/space/project/experiment for experiments or /space/sample for samples") + private String objectID; + @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<>(); + @Option(arity = "1", paramLabel = "dataset type", description = "The openBIS dataset type code the " + + "data should be stored as. UNKNOWN if no type is chosen.", names = {"-t", "--type"}) + private String datasetType = "UNKNOWN"; + @Mixin + OpenbisAuthenticationOptions auth = new OpenbisAuthenticationOptions(); + + private OpenbisConnector openbis; + + @Override + public void run() { + App.readConfig(); + + OpenBIS authentication = App.loginToOpenBIS(auth.getOpenbisPassword(), auth.getOpenbisUser(), auth.getOpenbisAS(), auth.getOpenbisDSS()); + openbis = new OpenbisConnector(authentication); + + if(!pathValid(dataPath)) { + System.out.printf("Path %s could not be found%n", dataPath); + return; + } + boolean attachToSample = false; + boolean attachToExperiment = openbis.experimentExists(objectID); + if(openbis.sampleExists(objectID)) { + attachToSample = true; + } + if(!attachToSample && !attachToExperiment) { + System.out.printf("%s could not be found in openBIS.%n", objectID); + return; + } + if(!datasetsExist(parents)) { + System.out.printf("One or more datasets %s could not be found%n", parents); + return; + } + System.out.println(); + System.out.println("Parameters verified, uploading dataset..."); + System.out.println(); + if(attachToExperiment) { + DataSetPermId result = openbis.registerDatasetForExperiment(Path.of(dataPath), objectID, + datasetType, parents); + System.out.printf("Dataset %s was successfully attached to experiment%n", result.getPermId()); + } else { + DataSetPermId result = openbis.registerDatasetForSample(Path.of(dataPath), objectID, + datasetType, parents); + System.out.printf("Dataset %s was successfully attached to sample%n", result.getPermId()); + } + } + + private boolean datasetsExist(List datasetCodes) { + return openbis.findDataSets(datasetCodes).size() == datasetCodes.size(); + } + + private boolean pathValid(String dataPath) { + return new File(dataPath).exists(); + } + + +} 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..1e7d5b8 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/UploadPetabResultCommand.java @@ -0,0 +1,113 @@ +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.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.Option; +import picocli.CommandLine.Parameters; + +/** + * The Upload PEtab command can be used to upload a PEtab Dataset to openBIS and connect it to its + * source files if these are stored in the same openBIS instance and referenced in the PEtabs meta- + * data. + * To upload a PEtab dataset, the path to the PEtab folder and the object ID to which it should + * be attached need to be provided. + * The dataset type of the new dataset in openBIS can be specified using the --type option, + * otherwise the type "UNKNOWN" will be used. + * The script will search the metaInformation.yaml for the entry "openBISSourceIds:" and attach the + * new dataset to all the datasets with ids in the following blocks found in this instance of + * openBIS: + * openBISSourceIds: + * - 20210702093837370-184137 + * - 20220702100912333-189138 + * If one or more dataset identifiers are not found, the script will stop without uploading the data + * and inform the user. + */ +@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 = "PEtab folder", description = "The path to the PEtab folder " + + "to upload") + private String dataPath; + @Parameters(arity = "1", paramLabel = "object ID", description = "The full identifier of the " + + "experiment or sample the data should be attached to. The identifier must be of the format: " + + "/space/project/experiment for experiments or /space/sample for samples") + private String objectID; + @Option(arity = "1", paramLabel = "dataset type", description = "The openBIS dataset type code the " + + "data should be stored as. UNKNOWN if no type is chosen.", names = {"-t", "--type"}) + private String datasetType = "UNKNOWN"; + @Mixin + OpenbisAuthenticationOptions auth = new OpenbisAuthenticationOptions(); + + private OpenbisConnector openbis; + private final PetabParser petabParser = new PetabParser(); + + @Override + public void run() { + App.readConfig(); + + OpenBIS authentication = App.loginToOpenBIS(auth.getOpenbisPassword(), auth.getOpenbisUser(), + auth.getOpenbisAS(), auth.getOpenbisDSS()); + openbis = new OpenbisConnector(authentication); + + if(!pathValid(dataPath)) { + System.out.printf("Path %s could not be found%n", dataPath); + return; + } + if(!new File(dataPath).isDirectory()) { + System.out.printf("%s is not a directory. Please specify the PETab directory root%n", dataPath); + return; + } + boolean attachToSample = false; + boolean attachToExperiment = openbis.experimentExists(objectID); + if(openbis.sampleExists(objectID)) { + attachToSample = true; + } + if(!attachToSample && !attachToExperiment) { + System.out.printf("%s could not be found in openBIS.%n", objectID); + return; + } + System.out.println("Looking for reference datasets in metaInformation.yaml..."); + List 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..."); + if(attachToExperiment) { + DataSetPermId result = openbis.registerDatasetForExperiment(Path.of(dataPath), objectID, + datasetType, parents); + System.out.printf("Dataset %s was successfully attached to experiment%n", result.getPermId()); + } else { + DataSetPermId result = openbis.registerDatasetForSample(Path.of(dataPath), objectID, + datasetType, parents); + System.out.printf("Dataset %s was successfully attached to sample%n", result.getPermId()); + } + } + + private boolean datasetsExist(List datasetCodes) { + return openbis.findDataSets(datasetCodes).size() == datasetCodes.size(); + } + + private boolean pathValid(String dataPath) { + return new File(dataPath).exists(); + } + +} diff --git a/src/main/java/life/qbic/model/AssetInformation.java b/src/main/java/life/qbic/model/AssetInformation.java new file mode 100644 index 0000000..476d65e --- /dev/null +++ b/src/main/java/life/qbic/model/AssetInformation.java @@ -0,0 +1,41 @@ +package life.qbic.model; + +public class AssetInformation { + + private String seekID; + private String title; + private String description; + private String assetType; + private String openbisPermId; + + public AssetInformation(String assetID, String assetType, String title, String description) { + this.seekID = assetID; + this.title = title; + this.description = description; + this.assetType = assetType; + } + + public String getAssetType() { + return assetType; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getSeekID() { + return seekID; + } + + public void setOpenbisPermId(String id) { + this.openbisPermId = id; + } + + public String getOpenbisPermId() { + return openbisPermId; + } +} diff --git a/src/main/java/life/qbic/model/Configuration.java b/src/main/java/life/qbic/model/Configuration.java new file mode 100644 index 0000000..cb567bd --- /dev/null +++ b/src/main/java/life/qbic/model/Configuration.java @@ -0,0 +1,13 @@ +package life.qbic.model; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Global configuration container + */ +public class Configuration { + + public static final long MAX_DOWNLOAD_ATTEMPTS = 3; + public static final Path LOG_PATH = Paths.get(System.getProperty("user.dir"),"logs"); +} diff --git a/src/main/java/life/qbic/model/DatasetWithProperties.java b/src/main/java/life/qbic/model/DatasetWithProperties.java new file mode 100644 index 0000000..a203d34 --- /dev/null +++ b/src/main/java/life/qbic/model/DatasetWithProperties.java @@ -0,0 +1,71 @@ +package life.qbic.model; + +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetType; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Wrapper class for openBIS DataSets that collects additional information, e.g. from samples, + * experiments etc. further up in the hierarchy. + */ +public class DatasetWithProperties { + + private final DataSet dataset; + private final Map properties; + + public DatasetWithProperties(DataSet dataset) { + this.dataset = dataset; + this.properties = new HashMap<>(); + } + + public void addProperty(String key, String value) { + this.properties.put(key, value); + } + + public String getProperty(String key) { + return properties.get(key); + } + + public Map getProperties() { + return properties; + } + + public DataSet getDataset() { + return dataset; + } + + public String getCode() { + return dataset.getCode(); + } + + public Experiment getExperiment() { + return dataset.getExperiment(); + } + + public DataSetType getType() { + return dataset.getType(); + } + + public Person getRegistrator() { + return dataset.getRegistrator(); + } + + public Date getRegistrationDate() { + return dataset.getRegistrationDate(); + } + + /** + * Returns sample ID or experiment ID, if Dataset has no sample. + */ + public String getClosestSourceID() { + if(dataset.getSample()!=null) { + return dataset.getSample().getIdentifier().getIdentifier(); + } else { + return getExperiment().getIdentifier().getIdentifier(); + } + } +} diff --git a/src/main/java/life/qbic/model/OpenbisExperimentWithDescendants.java b/src/main/java/life/qbic/model/OpenbisExperimentWithDescendants.java new file mode 100644 index 0000000..1c8baec --- /dev/null +++ b/src/main/java/life/qbic/model/OpenbisExperimentWithDescendants.java @@ -0,0 +1,39 @@ +package life.qbic.model; + +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import java.util.List; +import java.util.Map; + +public class OpenbisExperimentWithDescendants { + + private Experiment experiment; + private List samples; + private List datasets; + private Map> datasetCodeToFiles; + + public OpenbisExperimentWithDescendants(Experiment experiment, List samples, + List datasets, Map> datasetCodeToFiles) { + this.experiment = experiment; + this.samples = samples; + this.datasets = datasets; + this.datasetCodeToFiles = datasetCodeToFiles; + } + + public Experiment getExperiment() { + return experiment; + } + + public List getSamples() { + return samples; + } + + public List getDatasets() { + return datasets; + } + + public List getFilesForDataset(String permID) { + return datasetCodeToFiles.get(permID); + } +} diff --git a/src/main/java/life/qbic/model/OpenbisSeekTranslator.java b/src/main/java/life/qbic/model/OpenbisSeekTranslator.java new file mode 100644 index 0000000..2c83354 --- /dev/null +++ b/src/main/java/life/qbic/model/OpenbisSeekTranslator.java @@ -0,0 +1,341 @@ +package life.qbic.model; + +import static java.util.Map.entry; + +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.DataType; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.PropertyAssignment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +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 javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import life.qbic.App; +import life.qbic.io.PropertyReader; +import life.qbic.model.isa.GenericSeekAsset; +import life.qbic.model.isa.ISAAssay; +import life.qbic.model.isa.ISASample; +import life.qbic.model.isa.ISASampleType; +import life.qbic.model.isa.ISASampleType.SampleAttribute; +import life.qbic.model.isa.ISASampleType.SampleAttributeType; +import life.qbic.model.isa.SeekStructure; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class OpenbisSeekTranslator { + + private final String DEFAULT_PROJECT_ID; + private String INVESTIGATION_ID; + private String STUDY_ID; + private final String openBISBaseURL; + private Map experimentTypeToAssayClass; + private Map dataTypeToAttributeType; + private Map datasetTypeToAssetType; + private Map experimentTypeToAssayType; + + public OpenbisSeekTranslator(String openBISBaseURL, String defaultProjectID) + throws IOException, ParserConfigurationException, SAXException { + this.openBISBaseURL = openBISBaseURL; + this.DEFAULT_PROJECT_ID = defaultProjectID; + parseConfigs(); + if(!App.configProperties.containsKey("seek_openbis_sample_title")) { + throw new RuntimeException("Script can not be run, since 'seek_openbis_sample_title' was not " + + "provided."); + } + } + + /** + * Used for translation to RO-Crate, without SEEK connection + * @param openbisBaseURL + */ + public OpenbisSeekTranslator(String openbisBaseURL) + throws IOException, ParserConfigurationException, SAXException { + this.openBISBaseURL = openbisBaseURL; + this.DEFAULT_PROJECT_ID = null; + parseConfigs(); + } + + //Used for translation to RO-Crate, without SEEK connection + public SeekStructure translateForRO(OpenbisExperimentWithDescendants experiment, + Set blacklist, boolean transferData) throws URISyntaxException { + + Experiment exp = experiment.getExperiment(); + String expType = exp.getType().getCode(); + String title = exp.getCode()+" ("+exp.getPermId().getPermId()+")"; + String assayType = experimentTypeToAssayType.get(expType); + + if(assayType ==null || assayType.isBlank()) { + throw new RuntimeException("Could not find assay type for " + expType+". A mapping needs to " + + "be added to the respective properties file."); + } + ISAAssay assay = new ISAAssay(title, "-1", experimentTypeToAssayClass.get(expType), + new URI(assayType)); + + SeekStructure result = new SeekStructure(assay, exp.getIdentifier().getIdentifier()); + + for(Sample sample : experiment.getSamples()) { + SampleType sampleType = sample.getType(); + + //try to put all attributes into sample properties, as they should be a 1:1 mapping + Map typeCodesToNames = new HashMap<>(); + Set propertiesLinkingSamples = new HashSet<>(); + for (PropertyAssignment a : sampleType.getPropertyAssignments()) { + String code = a.getPropertyType().getCode(); + String label = a.getPropertyType().getLabel(); + DataType type = a.getPropertyType().getDataType(); + typeCodesToNames.put(code, label); + if(type.equals(DataType.SAMPLE)) { + propertiesLinkingSamples.add(code); + } + } + Map attributes = new HashMap<>(); + for(String code : sample.getProperties().keySet()) { + String value = sample.getProperty(code); + if(propertiesLinkingSamples.contains(code)) { + value = generateOpenBISLinkFromPermID("SAMPLE", value); + } + attributes.put(typeCodesToNames.get(code), value); + } + + String sampleID = sample.getIdentifier().getIdentifier(); + attributes.put(App.configProperties.get("seek_openbis_sample_title"), sampleID); + ISASample isaSample = new ISASample(sample.getIdentifier().getIdentifier(), attributes, + "-1", Collections.singletonList(DEFAULT_PROJECT_ID)); + result.addSample(isaSample, sampleID); + } + + //create ISA files for assets. If actual data is to be uploaded is determined later based on flag + for(DatasetWithProperties dataset : experiment.getDatasets()) { + String permID = dataset.getCode(); + if(!blacklist.contains(permID)) { + for(DataSetFile file : experiment.getFilesForDataset(permID)) { + String datasetType = getDatasetTypeOfFile(file, experiment.getDatasets()); + datasetFileToSeekAsset(file, datasetType, transferData) + .ifPresent(seekAsset -> result.addAsset(seekAsset, file)); + } + } + } + return result; + } + + /** + * Parses mandatory mapping information from mandatory config files. Other files may be added. + */ + private void parseConfigs() throws IOException, ParserConfigurationException, SAXException { + final String dataTypeToAttributeType = "openbis_datatype_to_seek_attributetype.xml"; + final String datasetToAssaytype = "dataset_type_to_asset_type.properties"; + final String experimentTypeToAssayClass = "experiment_type_to_assay_class.properties"; + final String experimentTypeToAssayType = "experiment_type_to_assay_type.properties"; + this.experimentTypeToAssayType = PropertyReader.getProperties(experimentTypeToAssayType); + this.datasetTypeToAssetType = PropertyReader.getProperties(datasetToAssaytype); + this.experimentTypeToAssayClass = PropertyReader.getProperties(experimentTypeToAssayClass); + this.dataTypeToAttributeType = parseAttributeXML(dataTypeToAttributeType); + } + + private Map parseAttributeXML(String dataTypeToAttributeType) + throws ParserConfigurationException, IOException, SAXException { + Map result = new HashMap<>(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(dataTypeToAttributeType); + NodeList elements = document.getElementsByTagName("entry"); + for (int i = 0; i < elements.getLength(); i++) { + Node node = elements.item(i); + DataType openbisType = DataType.valueOf(node.getAttributes() + .getNamedItem("type") + .getNodeValue()); + NodeList nodes = node.getChildNodes(); + String seekId = "", seekTitle = "", seekBaseType = ""; + for (int j = 0; j < nodes.getLength(); j++) { + Node n = nodes.item(j); + if (n.getNodeName().equals("seek_id")) { + seekId = n.getTextContent(); + } + if (n.getNodeName().equals("seek_type")) { + seekBaseType = n.getTextContent(); + } + if (n.getNodeName().equals("seek_title")) { + seekTitle = n.getTextContent(); + } + } + result.put(openbisType, new SampleAttributeType(seekId, seekTitle, seekBaseType)); + } + return result; + } + + private String generateOpenBISLinkFromPermID(String entityType, String permID) { + StringBuilder builder = new StringBuilder(); + builder.append(openBISBaseURL); + builder.append("#entity="); + builder.append(entityType); + builder.append("&permId="); + builder.append(permID); + return builder.toString(); + } + + /** + * not mandatory, but nice to have? + */ + Map fileExtensionToDataFormat = Map.ofEntries( + entry("fastq.gz", "http://edamontology.org/format_1930"), + entry("fastq", "http://edamontology.org/format_1930"), + entry("json", "http://edamontology.org/format_3464"), + entry("yaml", "http://edamontology.org/format_3750"), + entry("raw", "http://edamontology.org/format_3712"), + entry("tsv", "http://edamontology.org/format_3475"), + entry("csv", "http://edamontology.org/format_3752"), + entry("txt", "http://edamontology.org/format_2330") + ); + + public ISASampleType translate(SampleType sampleType) { + SampleAttribute titleAttribute = new SampleAttribute( + App.configProperties.get("seek_openbis_sample_title"), + dataTypeToAttributeType.get(DataType.VARCHAR), true, false); + ISASampleType type = new ISASampleType(sampleType.getCode(), titleAttribute, + DEFAULT_PROJECT_ID); + for (PropertyAssignment a : sampleType.getPropertyAssignments()) { + DataType dataType = a.getPropertyType().getDataType(); + type.addSampleAttribute(a.getPropertyType().getLabel(), dataTypeToAttributeType.get(dataType), + false, null); + } + return type; + } + + public String assetForDatasetType(String datasetType) { + if(datasetTypeToAssetType.get(datasetType) == null || datasetTypeToAssetType.get(datasetType).isBlank()) { + throw new RuntimeException("Dataset type " + datasetType + " could not be mapped to SEEK type."); + } + return datasetTypeToAssetType.get(datasetType); + } + + public String dataFormatAnnotationForExtension(String fileExtension) { + return fileExtensionToDataFormat.get(fileExtension); + } + + public SeekStructure translate(OpenbisExperimentWithDescendants experiment, + Map sampleTypesToIds, Set blacklist, boolean transferData) + throws URISyntaxException { + + Experiment exp = experiment.getExperiment(); + String expType = exp.getType().getCode(); + String title = exp.getCode()+" ("+exp.getPermId().getPermId()+")"; + String assayType = experimentTypeToAssayType.get(expType); + + if(assayType ==null || assayType.isBlank()) { + throw new RuntimeException("Could not find assay type for " + expType+". A mapping needs to " + + "be added to the respective properties file."); + } + ISAAssay assay = new ISAAssay(title, STUDY_ID, experimentTypeToAssayClass.get(expType), + new URI(assayType)); + + SeekStructure result = new SeekStructure(assay, exp.getIdentifier().getIdentifier()); + + for(Sample sample : experiment.getSamples()) { + SampleType sampleType = sample.getType(); + + //try to put all attributes into sample properties, as they should be a 1:1 mapping + Map typeCodesToNames = new HashMap<>(); + Set propertiesLinkingSamples = new HashSet<>(); + for (PropertyAssignment a : sampleType.getPropertyAssignments()) { + String code = a.getPropertyType().getCode(); + String label = a.getPropertyType().getLabel(); + DataType type = a.getPropertyType().getDataType(); + typeCodesToNames.put(code, label); + if(type.equals(DataType.SAMPLE)) { + propertiesLinkingSamples.add(code); + } + } + Map attributes = new HashMap<>(); + for(String code : sample.getProperties().keySet()) { + String value = sample.getProperty(code); + if(propertiesLinkingSamples.contains(code)) { + value = generateOpenBISLinkFromPermID("SAMPLE", value); + } + attributes.put(typeCodesToNames.get(code), value); + } + + String sampleID = sample.getIdentifier().getIdentifier(); + attributes.put(App.configProperties.get("seek_openbis_sample_title"), sampleID); + String sampleTypeId = sampleTypesToIds.get(sampleType.getCode()); + ISASample isaSample = new ISASample(sample.getIdentifier().getIdentifier(), attributes, + sampleTypeId, Collections.singletonList(DEFAULT_PROJECT_ID)); + result.addSample(isaSample, sampleID); + } + + //create ISA files for assets. If actual data is to be uploaded is determined later based on flag + for(DatasetWithProperties dataset : experiment.getDatasets()) { + String permID = dataset.getCode(); + if(!blacklist.contains(permID)) { + for(DataSetFile file : experiment.getFilesForDataset(permID)) { + String datasetType = getDatasetTypeOfFile(file, experiment.getDatasets()); + datasetFileToSeekAsset(file, datasetType, transferData) + .ifPresent(seekAsset -> result.addAsset(seekAsset, file)); + } + } + } + return result; + } + + private String getDatasetTypeOfFile(DataSetFile file, List dataSets) { + String permId = file.getDataSetPermId().getPermId(); + for(DatasetWithProperties dataset : dataSets) { + if(dataset.getCode().equals(permId)) { + return dataset.getType().getCode(); + } + } + return ""; + } + + /** + * Creates a SEEK asset from an openBIS DataSetFile, if it describes a file (not a folder). + * @param file the openBIS DataSetFile + * @return an optional SEEK asset + */ + private Optional datasetFileToSeekAsset(DataSetFile file, String datasetType, + boolean transferData) { + if (!file.getPath().isBlank() && !file.isDirectory()) { + File f = new File(file.getPath()); + String datasetPermID = file.getDataSetPermId().toString(); + String assetName = datasetPermID + ": " + f.getName(); + String assetType = assetForDatasetType(datasetType); + GenericSeekAsset isaFile = new GenericSeekAsset(assetType, assetName, file.getPath(), + Arrays.asList(DEFAULT_PROJECT_ID), file.getFileLength()); + //reference the openbis dataset in the description - if transferData flag is false, this will + //also add a second link instead of the (non-functional) download link to a non-existent blob. + //it seems that directly linking to files needs an open session, so we only set the dataset for now + String datasetLink = generateOpenBISLinkFromPermID("DATA_SET", datasetPermID); + isaFile.setDatasetLink(datasetLink, transferData); + String fileExtension = f.getName().substring(f.getName().lastIndexOf(".") + 1); + String annotation = dataFormatAnnotationForExtension(fileExtension); + if (annotation != null) { + isaFile.withDataFormatAnnotations(Arrays.asList(annotation)); + } + return Optional.of(isaFile); + } + return Optional.empty(); + } + + public void setDefaultStudy(String studyID) { + this.STUDY_ID = studyID; + } + + public void setDefaultInvestigation(String investigationID) { + this.INVESTIGATION_ID = investigationID; + } + +} diff --git a/src/main/java/life/qbic/model/SampleInformation.java b/src/main/java/life/qbic/model/SampleInformation.java new file mode 100644 index 0000000..73a264b --- /dev/null +++ b/src/main/java/life/qbic/model/SampleInformation.java @@ -0,0 +1,28 @@ +package life.qbic.model; + +import java.util.Map; + +public class SampleInformation { + + private String seekID; + private String openBisIdentifier; + private Map attributes; + + public SampleInformation(String sampleID, String title, Map attributesMap) { + this.seekID = sampleID; + this.openBisIdentifier = title; + this.attributes = attributesMap; + } + + public String getSeekID() { + return seekID; + } + + public Map getAttributes() { + return attributes; + } + + public String getOpenBisIdentifier() { + return openBisIdentifier; + } +} diff --git a/src/main/java/life/qbic/model/SampleTypeConnection.java b/src/main/java/life/qbic/model/SampleTypeConnection.java new file mode 100644 index 0000000..807abce --- /dev/null +++ b/src/main/java/life/qbic/model/SampleTypeConnection.java @@ -0,0 +1,69 @@ +package life.qbic.model; + +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType; +import java.util.Objects; + +public class SampleTypeConnection { + + private SampleType parent; + private SampleType child; + + public SampleTypeConnection(SampleType parentType, SampleType childType) { + this.parent = parentType; + this.child = childType; + } + + public SampleTypeConnection(SampleType parentType) { + this(parentType, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SampleTypeConnection)) { + return false; + } + + SampleTypeConnection that = (SampleTypeConnection) o; + if(child == null || that.child == null) { + boolean a = Objects.equals(child, that.child); + if(parent == null || that.parent == null) { + return a && Objects.equals(parent, that.parent); + } else { + return a && Objects.equals(parent.getCode(), that.parent.getCode()); + } + } + if(parent == null || that.parent == null) { + boolean a = Objects.equals(parent, that.parent); + if(child == null || that.child == null) { + return a && Objects.equals(child, that.child); + } else { + return a && Objects.equals(child.getCode(), that.child.getCode()); + } + } + + if (!Objects.equals(parent.getCode(), that.parent.getCode())) { + return false; + } + return Objects.equals(child.getCode(), that.child.getCode()); + } + + @Override + public int hashCode() { + int result = parent != null ? parent.getCode().hashCode() : 0; + result = 31 * result + (child != null ? child.getCode().hashCode() : 0); + return result; + } + + @Override + public String toString() { + String parentCode = parent.getCode(); + if(child==null) { + return parentCode; + } else { + return parentCode+" -> "+child.getCode(); + } + } +} diff --git a/src/main/java/life/qbic/model/SampleTypesAndMaterials.java b/src/main/java/life/qbic/model/SampleTypesAndMaterials.java new file mode 100644 index 0000000..8a82576 --- /dev/null +++ b/src/main/java/life/qbic/model/SampleTypesAndMaterials.java @@ -0,0 +1,23 @@ +package life.qbic.model; + +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType; +import java.util.Set; + +public class SampleTypesAndMaterials { + + Set sampleTypes; + Set sampleTypesAsMaterials; + + public SampleTypesAndMaterials(Set sampleTypes, Set sampleTypesAsMaterials) { + this.sampleTypes = sampleTypes; + this.sampleTypesAsMaterials = sampleTypesAsMaterials; + } + + public Set getSamplesAsMaterials() { + return sampleTypesAsMaterials; + } + + public Set getSampleTypes() { + return sampleTypes; + } +} diff --git a/src/main/java/life/qbic/model/download/AuthenticationException.java b/src/main/java/life/qbic/model/download/AuthenticationException.java new file mode 100644 index 0000000..6738853 --- /dev/null +++ b/src/main/java/life/qbic/model/download/AuthenticationException.java @@ -0,0 +1,23 @@ +package life.qbic.model.download; + +/** + * Exception to indicate failed authentication against openBIS. + *

+ * This exception shall be thrown, when the returned session token of openBIS is empty, after the + * client tried to authenticate against the openBIS application server via its Java API. + */ +public class AuthenticationException extends RuntimeException { + + AuthenticationException() { + super(); + } + + AuthenticationException(String msg) { + super(msg); + } + + AuthenticationException(String msg, Throwable t) { + super(msg, t); + } + +} diff --git a/src/main/java/life/qbic/model/download/ConnectionException.java b/src/main/java/life/qbic/model/download/ConnectionException.java new file mode 100644 index 0000000..73bf9af --- /dev/null +++ b/src/main/java/life/qbic/model/download/ConnectionException.java @@ -0,0 +1,20 @@ +package life.qbic.model.download; + +/** + * ConnectionException indicates issues when a client wants to connect with the openBIS API. + */ +public class ConnectionException extends RuntimeException { + + ConnectionException() { + super(); + } + + ConnectionException(String msg) { + super(msg); + } + + ConnectionException(String msg, Throwable t) { + super(msg, t); + } + +} diff --git a/src/main/java/life/qbic/model/download/FileSystemWriter.java b/src/main/java/life/qbic/model/download/FileSystemWriter.java new file mode 100644 index 0000000..59b6419 --- /dev/null +++ b/src/main/java/life/qbic/model/download/FileSystemWriter.java @@ -0,0 +1,53 @@ +package life.qbic.model.download; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +/** + * File system implementation of the ModelReporter interface. + * + * Provides methods to write the queried openBIS model to a file on the local file system. + * + * @author: Sven Fillinger, Andreas Friedrich + */ +public class FileSystemWriter implements SummaryWriter { + + /** + * File that stores the summary report content for valid checksums. + */ + final private File summaryFile; + + + /** + * FileSystemWriter constructor with the paths for the summary files. + * + * @param summaryFile The path where to write the summary + */ + public FileSystemWriter(Path summaryFile) { + this.summaryFile = new File(summaryFile.toString()); + } + + /** + * {@inheritDoc} + */ + @Override + public void reportSummary(List summary) throws IOException { + if (!summaryFile.exists()) { + summaryFile.createNewFile(); + //file exists or could not be created + if (!summaryFile.exists()) { + throw new IOException("The file " + summaryFile.getAbsoluteFile() + " could not be created."); + } + } + BufferedWriter writer = new BufferedWriter(new FileWriter(summaryFile, true)); + for(String line : summary) { + writer.append(line+"\n"); + } + writer.close(); + } + +} diff --git a/src/main/java/life/qbic/model/download/OpenbisConnector.java b/src/main/java/life/qbic/model/download/OpenbisConnector.java new file mode 100644 index 0000000..7d872eb --- /dev/null +++ b/src/main/java/life/qbic/model/download/OpenbisConnector.java @@ -0,0 +1,729 @@ +package life.qbic.model.download; + +import ch.ethz.sis.openbis.generic.OpenBIS; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetType; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetTypeFetchOptions; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.search.DataSetSearchCriteria; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.search.DataSetTypeSearchCriteria; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.update.DataSetUpdate; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.EntityKind; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentSearchCriteria; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentUpdate; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.create.SampleCreation; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleTypeFetchOptions; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.search.SampleSearchCriteria; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.search.SampleTypeSearchCriteria; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.update.SampleUpdate; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.search.SpaceSearchCriteria; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.dataset.create.UploadedDataSetCreation; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownload; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadOptions; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadReader; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.fetchoptions.DataSetFileFetchOptions; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.id.DataSetFilePermId; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.id.IDataSetFileId; +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.search.DataSetFileSearchCriteria; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +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.regex.Pattern; +import java.util.stream.Collectors; +import life.qbic.model.DatasetWithProperties; +import life.qbic.model.OpenbisExperimentWithDescendants; +import life.qbic.model.SampleTypeConnection; +import life.qbic.model.SampleTypesAndMaterials; +import life.qbic.model.download.SEEKConnector.SeekStructurePostRegistrationInformation; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class OpenbisConnector { + + private static final Logger LOG = LogManager.getLogger(OpenbisConnector.class); + private final OpenBIS openBIS; + + public static Pattern datasetCodePattern = Pattern.compile("[0-9]{17}-[0-9]+"); + public final String EXPERIMENT_LINK_PROPERTY = "EXPERIMENT_NAME"; + public final String SAMPLE_LINK_PROPERTY = "experimentLink"; + public final String DATASET_LINK_PROPERTY = "experimentLink"; + + public OpenbisConnector(OpenBIS authentication) { + this.openBIS = authentication; + } + + public List getSpaces() { + SpaceSearchCriteria criteria = new SpaceSearchCriteria(); + SpaceFetchOptions options = new SpaceFetchOptions(); + return openBIS.searchSpaces(criteria, options).getObjects() + .stream().map(Space::getCode).collect(Collectors.toList()); + } + + public DataSetPermId registerDatasetForExperiment(Path uploadPath, String experimentID, + String datasetType, List parentCodes) { + UploadedDataSetCreation creation = prepareDataSetCreation(uploadPath, datasetType, parentCodes); + creation.setExperimentId(new ExperimentIdentifier(experimentID)); + + try { + return openBIS.createUploadedDataSet(creation); + } catch (final Exception e) { + LOG.error(e.getMessage()); + } + return null; + } + + public DataSetPermId registerDatasetForSample(Path uploadPath, String sampleID, + String datasetType, List parentCodes) { + UploadedDataSetCreation creation = prepareDataSetCreation(uploadPath, datasetType, parentCodes); + creation.setSampleId(new SampleIdentifier(sampleID)); + + try { + return openBIS.createUploadedDataSet(creation); + } catch (final Exception e) { + LOG.error(e.getMessage()); + } + return null; + } + + private UploadedDataSetCreation prepareDataSetCreation(Path uploadPath, String datasetType, + List parentCodes) { + if(listDatasetTypes().stream().map(DataSetType::getCode).noneMatch(x -> x.equals(datasetType))) { + throw new RuntimeException("Dataset type " + datasetType + + " is not supported by this instance of openBIS."); + } + final String uploadId = openBIS.uploadFileWorkspaceDSS(uploadPath); + + final UploadedDataSetCreation creation = new UploadedDataSetCreation(); + creation.setUploadId(uploadId); + creation.setParentIds(parentCodes.stream().map(DataSetPermId::new).collect( + Collectors.toList())); + creation.setTypeId(new EntityTypePermId(datasetType, EntityKind.DATA_SET)); + return creation; + } + + private static void copyInputStreamToFile(InputStream inputStream, File file) + throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file, false)) { + int read; + byte[] bytes = new byte[8192]; + while ((read = inputStream.read(bytes)) != -1) { + outputStream.write(bytes, 0, read); + } + } + } + + public List listDatasetsOfExperiment(List spaces, String experiment) { + DataSetSearchCriteria criteria = new DataSetSearchCriteria(); + criteria.withExperiment().withCode().thatEquals(experiment); + if (!spaces.isEmpty()) { + criteria.withAndOperator(); + criteria.withExperiment().withProject().withSpace().withCodes().thatIn(spaces); + } + DataSetFetchOptions options = new DataSetFetchOptions(); + options.withType(); + options.withRegistrator(); + options.withExperiment().withProject().withSpace(); + return openBIS.searchDataSets(criteria, options).getObjects(); + } + + public List listDatasetsOfSample(List spaces, String sample) { + DataSetSearchCriteria criteria = new DataSetSearchCriteria(); + criteria.withSample().withCode().thatEquals(sample); + if (!spaces.isEmpty()) { + criteria.withAndOperator(); + criteria.withExperiment().withProject().withSpace().withCodes().thatIn(spaces); + } + DataSetFetchOptions options = new DataSetFetchOptions(); + options.withType(); + options.withRegistrator(); + options.withExperiment().withProject().withSpace(); + return openBIS.searchDataSets(criteria, options).getObjects(); + } + + public File downloadDataset(String targetPath, String datasetID, String filePath) { + DataSetFileDownloadOptions options = new DataSetFileDownloadOptions(); + IDataSetFileId fileToDownload = new DataSetFilePermId(new DataSetPermId(datasetID), + filePath); + + // Setting recursive flag to true will return both subfolders and files + options.setRecursive(true); + + // Read the contents and print them out + InputStream stream = openBIS.downloadFiles(new ArrayList<>(List.of(fileToDownload)), + options); + DataSetFileDownloadReader reader = new DataSetFileDownloadReader(stream); + DataSetFileDownload file; + while ((file = reader.read()) != null) { + DataSetFile df = file.getDataSetFile(); + String currentPath = df.getPath().replace("original", ""); + if (df.isDirectory()) { + File newDir = new File(targetPath, currentPath); + if (!newDir.exists()) { + if(!newDir.mkdirs()) { + throw new RuntimeException("Could not create folders for downloaded dataset."); + } + } + } else { + File toWrite = new File(targetPath, currentPath); + try { + copyInputStreamToFile(file.getInputStream(), toWrite); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + return new File(targetPath, filePath.replace("original/","")); + } + + public InputStream streamDataset(String datasetCode, String filePath) { + DataSetFileDownloadOptions options = new DataSetFileDownloadOptions(); + IDataSetFileId fileToDownload = new DataSetFilePermId(new DataSetPermId(datasetCode), + filePath); + + // Setting recursive flag to true will return both subfolders and files + options.setRecursive(true); + + // Read the contents and print them out + InputStream stream = openBIS.downloadFiles(new ArrayList<>(List.of(fileToDownload)), + options); + + DataSetFileDownloadReader reader = new DataSetFileDownloadReader(stream); + return reader.read().getInputStream(); + } + + public Map queryFullSampleHierarchy(List spaces) { + Map hierarchy = new HashMap<>(); + if (spaces.isEmpty()) { + spaces = getSpaces(); + } + for (String space : spaces) { + SampleFetchOptions fetchType = new SampleFetchOptions(); + fetchType.withType(); + SampleFetchOptions withDescendants = new SampleFetchOptions(); + withDescendants.withChildrenUsing(fetchType); + withDescendants.withType(); + SampleSearchCriteria criteria = new SampleSearchCriteria(); + criteria.withSpace().withCode().thatEquals(space.toUpperCase()); + SearchResult result = openBIS.searchSamples(criteria, withDescendants); + for (Sample s : result.getObjects()) { + SampleType parentType = s.getType(); + List children = s.getChildren(); + if (children.isEmpty()) { + SampleTypeConnection leaf = new SampleTypeConnection(parentType); + if (hierarchy.containsKey(leaf)) { + int count = hierarchy.get(leaf) + 1; + hierarchy.put(leaf, count); + } else { + hierarchy.put(leaf, 1); + } + } else { + for (Sample c : children) { + SampleType childType = c.getType(); + SampleTypeConnection connection = new SampleTypeConnection(parentType, childType); + if (hierarchy.containsKey(connection)) { + int count = hierarchy.get(connection) + 1; + hierarchy.put(connection, count); + } else { + hierarchy.put(connection, 1); + } + } + } + } + } + return hierarchy; + } + + private Set getPropertiesFromSampleHierarchy(String propertyName, List samples, + Set foundProperties) { + if(samples.isEmpty()) { + return foundProperties; + } + for(Sample s : samples) { + if(s.getProperties().containsKey(propertyName)) { + foundProperties.add(s.getProperties().get(propertyName)); + } + } + return getPropertiesFromSampleHierarchy(propertyName, + samples.stream().map(Sample::getParents).flatMap(List::stream).collect(Collectors.toList()), + foundProperties); + } + + public Set findPropertiesInSampleHierarchy(String propertyName, + ExperimentIdentifier experimentId) { + return getPropertiesFromSampleHierarchy(propertyName, + getSamplesWithAncestorsOfExperiment(experimentId), new HashSet<>()); + } + + public Map> getExperimentsBySpace(List spaces) { + Map> result = new HashMap<>(); + ExperimentFetchOptions options = new ExperimentFetchOptions(); + options.withProject().withSpace(); + ExperimentSearchCriteria criteria = new ExperimentSearchCriteria(); + criteria.withProject().withSpace().withCodes().thatIn(spaces); + for (Experiment e : openBIS.searchExperiments(criteria, options).getObjects()) { + String space = e.getProject().getSpace().getCode(); + if(result.containsKey(space)) { + result.get(space).add(e); + } else { + result.put(space, new ArrayList<>()); + } + } + return result; + } + + public Map> getSamplesBySpace(List spaces) { + Map> result = new HashMap<>(); + SampleFetchOptions options = new SampleFetchOptions(); + options.withSpace(); + SampleSearchCriteria criteria = new SampleSearchCriteria(); + criteria.withSpace().withCodes().thatIn(spaces); + for (Sample s : openBIS.searchSamples(criteria, options).getObjects()) { + String space = s.getSpace().getCode(); + if(!result.containsKey(space)) { + result.put(space, new ArrayList<>()); + } + result.get(space).add(s); + } + return result; + } + + public Map>> getExperimentsByTypeAndSpace(List spaces) { + Map>> result = new HashMap<>(); + ExperimentFetchOptions options = new ExperimentFetchOptions(); + options.withProject().withSpace(); + options.withType(); + + ExperimentSearchCriteria criteria = new ExperimentSearchCriteria(); + criteria.withProject().withSpace().withCodes().thatIn(spaces); + for (Experiment exp : openBIS.searchExperiments(criteria, options).getObjects()) { + String space = exp.getProject().getSpace().getCode(); + String type = exp.getType().getCode(); + if(!result.containsKey(space)) { + Map> typeMap = new HashMap<>(); + typeMap.put(type, new ArrayList<>(Arrays.asList(exp))); + result.put(space, typeMap); + } else { + Map> typeMap = result.get(space); + if(!typeMap.containsKey(type)) { + typeMap.put(type, new ArrayList<>()); + } + typeMap.get(type).add(exp); + } + } + return result; + } + + public Map>> getSamplesByTypeAndSpace(List spaces) { + Map>> result = new HashMap<>(); + SampleFetchOptions options = new SampleFetchOptions(); + options.withSpace(); + options.withType(); + + SampleSearchCriteria criteria = new SampleSearchCriteria(); + criteria.withSpace().withCodes().thatIn(spaces); + for (Sample s : openBIS.searchSamples(criteria, options).getObjects()) { + String space = s.getSpace().getCode(); + String type = s.getType().getCode(); + if(!result.containsKey(space)) { + Map> typeMap = new HashMap<>(); + typeMap.put(type, new ArrayList<>(Arrays.asList(s))); + result.put(space, typeMap); + } else { + Map> typeMap = result.get(space); + if(!typeMap.containsKey(type)) { + typeMap.put(type, new ArrayList<>()); + } + typeMap.get(type).add(s); + } + } + return result; + } + + public Map>> getDatasetsByTypeAndSpace(List spaces) { + Map>> result = new HashMap<>(); + DataSetFetchOptions options = new DataSetFetchOptions(); + options.withSample().withSpace(); + options.withExperiment().withProject().withSpace(); + options.withType(); + DataSetSearchCriteria criteria = new DataSetSearchCriteria(); + criteria.withOrOperator(); + criteria.withSample().withSpace().withCodes().thatIn(spaces); + criteria.withExperiment().withProject().withSpace().withCodes().thatIn(spaces); + for (DataSet d : openBIS.searchDataSets(criteria, options).getObjects()) { + String space = getSpaceFromSampleOrExperiment(d); + String type = d.getType().getCode(); + if(!result.containsKey(space)) { + Map> typeMap = new HashMap<>(); + typeMap.put(type, new ArrayList<>(Arrays.asList(d))); + result.put(space, typeMap); + } else { + Map> typeMap = result.get(space); + if(!typeMap.containsKey(type)) { + typeMap.put(type, new ArrayList<>()); + } + typeMap.get(type).add(d); + } + } + return result; + } + + private String getSpaceFromSampleOrExperiment(DataSet d) { + try { + if (d.getSample() != null) { + return d.getSample().getSpace().getCode(); + } + if (d.getExperiment() != null) { + return d.getExperiment().getProject().getSpace().getCode(); + } + } catch (NullPointerException e) { + + } + System.out.println("Dataset " + d + "does not seem to be attached to a space"); + return "NO SPACE"; + } + + private List getSamplesWithAncestorsOfExperiment(ExperimentIdentifier experimentId) { + int numberOfFetchedLevels = 10; + SampleFetchOptions previousLevel = null; + for(int i = 0; i < numberOfFetchedLevels; i++) { + SampleFetchOptions withAncestors = new SampleFetchOptions(); + withAncestors.withProperties(); + withAncestors.withType(); + if (previousLevel != null) { + withAncestors.withParentsUsing(previousLevel); + } + previousLevel = withAncestors; + } + + SampleSearchCriteria criteria = new SampleSearchCriteria(); + criteria.withExperiment().withId().thatEquals(experimentId); + + return openBIS.searchSamples(criteria, previousLevel).getObjects(); + } + + public List findDataSets(List codes) { + if (codes.isEmpty()) { + return new ArrayList<>(); + } + DataSetSearchCriteria criteria = new DataSetSearchCriteria(); + criteria.withCodes().thatIn(codes); + DataSetFetchOptions options = new DataSetFetchOptions(); + options.withExperiment(); + options.withType(); + return openBIS.searchDataSets(criteria, options).getObjects(); + } + + public boolean datasetExists(String code) { + return !findDataSets(new ArrayList<>(Arrays.asList(code))).isEmpty(); + } + + public boolean experimentExists(String experimentID) { + ExperimentSearchCriteria criteria = new ExperimentSearchCriteria(); + criteria.withIdentifier().thatEquals(experimentID); + + return !openBIS.searchExperiments(criteria, new ExperimentFetchOptions()).getObjects() + .isEmpty(); + } + + public boolean sampleExists(String objectID) { + SampleSearchCriteria criteria = new SampleSearchCriteria(); + criteria.withIdentifier().thatEquals(objectID); + + return !openBIS.searchSamples(criteria, new SampleFetchOptions()).getObjects() + .isEmpty(); + } + + public OpenbisExperimentWithDescendants getExperimentWithDescendants(String experimentID) { + ExperimentSearchCriteria criteria = new ExperimentSearchCriteria(); + criteria.withIdentifier().thatEquals(experimentID); + + ExperimentFetchOptions fetchOptions = new ExperimentFetchOptions(); + fetchOptions.withType(); + fetchOptions.withProject(); + fetchOptions.withProperties(); + DataSetFetchOptions dataSetFetchOptions = new DataSetFetchOptions(); + dataSetFetchOptions.withType(); + dataSetFetchOptions.withRegistrator(); + dataSetFetchOptions.withExperiment(); + dataSetFetchOptions.withSample(); + SampleFetchOptions sampleFetchOptions = new SampleFetchOptions(); + sampleFetchOptions.withProperties(); + sampleFetchOptions.withType().withPropertyAssignments().withPropertyType(); + sampleFetchOptions.withDataSetsUsing(dataSetFetchOptions); + fetchOptions.withDataSetsUsing(dataSetFetchOptions); + fetchOptions.withSamplesUsing(sampleFetchOptions); + + Experiment experiment = openBIS.searchExperiments(criteria, fetchOptions).getObjects().get(0); + + Map> datasetCodeToFiles = new HashMap<>(); + for(DataSet dataset : experiment.getDataSets()) { + datasetCodeToFiles.put(dataset.getPermId().getPermId(), getDatasetFiles(dataset)); + } + + return new OpenbisExperimentWithDescendants(experiment, experiment.getSamples(), + experiment.getDataSets() + .stream().map(DatasetWithProperties::new) + .collect(Collectors.toList()), datasetCodeToFiles); + } + + public List getDatasetFiles(DataSet dataset) { + DataSetFileSearchCriteria criteria = new DataSetFileSearchCriteria(); + + DataSetSearchCriteria dataSetCriteria = criteria.withDataSet().withOrOperator(); + dataSetCriteria.withCode().thatEquals(dataset.getCode()); + + SearchResult result = openBIS.searchFiles(criteria, new DataSetFileFetchOptions()); + + return result.getObjects(); + } + + public List listDatasetTypes() { + DataSetTypeSearchCriteria criteria = new DataSetTypeSearchCriteria(); + DataSetTypeFetchOptions fetchOptions = new DataSetTypeFetchOptions(); + fetchOptions.withPropertyAssignments().withPropertyType(); + fetchOptions.withPropertyAssignments().withEntityType(); + return openBIS.searchDataSetTypes(criteria, fetchOptions).getObjects(); + } + + public SampleTypesAndMaterials getSampleTypesWithMaterials() { + SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria(); + SampleTypeFetchOptions typeOptions = new SampleTypeFetchOptions(); + typeOptions.withPropertyAssignments().withPropertyType(); + typeOptions.withPropertyAssignments().withEntityType(); + Set sampleTypes = new HashSet<>(); + Set sampleTypesAsMaterials = new HashSet<>(); + for(SampleType type : openBIS.searchSampleTypes(criteria, typeOptions).getObjects()) { + /* + System.err.println("sample type: "+type.getCode()); + for(PropertyAssignment assignment : type.getPropertyAssignments()) { + if (assignment.getPropertyType().getDataType().name().equals("SAMPLE")) { + System.err.println(assignment.getPropertyType().getLabel()); + System.err.println(assignment.getPropertyType().getDataType().name()); + System.err.println(assignment.getPropertyType().getCode()); + } + } + */ + if(type.getCode().startsWith("MATERIAL.")) { + sampleTypesAsMaterials.add(type); + } else { + sampleTypes.add(type); + } + } + return new SampleTypesAndMaterials(sampleTypes, sampleTypesAsMaterials); + } + + public void createSeekLinks(SeekStructurePostRegistrationInformation postRegInformation) { + Optional> experimentInfo = postRegInformation.getExperimentIDWithEndpoint(); + //TODO link sample type not implemented? + final String SAMPLE_TYPE = "EXTERNAL_LINK"; + + SampleTypeSearchCriteria criteria = new SampleTypeSearchCriteria(); + criteria.withCode().thatEquals(SAMPLE_TYPE); + SampleTypeFetchOptions typeOptions = new SampleTypeFetchOptions(); + typeOptions.withPropertyAssignments().withPropertyType(); + typeOptions.withPropertyAssignments().withEntityType(); + if(openBIS.searchSampleTypes(criteria, typeOptions).getObjects().isEmpty()) { + System.out.printf( + "This is where links would be put into openBIS, but EXTERNAL_LINK sample was " + + "not yet added to openBIS instance.%n"); + return; + } + + if(experimentInfo.isPresent()) { + ExperimentIdentifier id = new ExperimentIdentifier(experimentInfo.get().getLeft()); + String endpoint = experimentInfo.get().getRight(); + SampleCreation sample = createNewLinkSample(endpoint); + sample.setExperimentId(id); + openBIS.createSamples(Arrays.asList(sample)); + } + Map sampleInfos = postRegInformation.getSampleIDsWithEndpoints(); + for(String sampleID : sampleInfos.keySet()) { + SampleIdentifier id = new SampleIdentifier(sampleID); + String endpoint = sampleInfos.get(sampleID); + SampleCreation sample = createNewLinkSample(endpoint); + sample.setParentIds(Arrays.asList(id)); + openBIS.createSamples(Arrays.asList(sample)); + } + } + + private SampleCreation createNewLinkSample(String endpoint) { + final String SAMPLE_TYPE = "EXTERNAL_LINK"; + SampleCreation sample = new SampleCreation(); + sample.setTypeId(new EntityTypePermId(SAMPLE_TYPE, EntityKind.SAMPLE)); + + Map properties = new HashMap<>(); + properties.put("LINK_TYPE", "SEEK"); + properties.put("URL", endpoint); + + sample.setProperties(properties); + return sample; + } + + public void updateSeekLinks(SeekStructurePostRegistrationInformation postRegistrationInformation) { + } + + private void updateExperimentProperties(ExperimentIdentifier id, Map properties, + boolean overwrite) { + ExperimentUpdate update = new ExperimentUpdate(); + update.setExperimentId(id); + if(overwrite) { + update.setProperties(properties); + } else { + ExperimentFetchOptions options = new ExperimentFetchOptions(); + options.withProperties(); + Experiment oldExp = openBIS.getExperiments(Arrays.asList(id), options).get(id); + for(String property : properties.keySet()) { + String newValue = properties.get(property); + String oldValue = oldExp.getProperty(property); + if(oldValue == null || oldValue.isEmpty() || oldValue.equals(newValue)) { + update.setProperty(property, newValue); + } else if(!newValue.isBlank()) { + update.setProperty(property, oldValue+", "+newValue);//TODO this can be changed to any other strategy + } + } + } + openBIS.updateExperiments(Arrays.asList(update)); + } + + private void updateSampleProperties(SampleIdentifier id, Map properties, + boolean overwrite) { + SampleUpdate update = new SampleUpdate(); + update.setSampleId(id); + if(overwrite) { + update.setProperties(properties); + } else { + SampleFetchOptions options = new SampleFetchOptions(); + options.withProperties(); + Sample oldSample = openBIS.getSamples(Arrays.asList(id), options).get(id); + for(String property : properties.keySet()) { + String newValue = properties.get(property); + String oldValue = oldSample.getProperty(property); + if(oldValue == null || oldValue.isEmpty() || oldValue.equals(newValue)) { + update.setProperty(property, newValue); + } else { + update.setProperty(property, oldValue+", "+newValue);//TODO this can be changed to any other strategy + } + } + } + openBIS.updateSamples(Arrays.asList(update)); + } + + private void updateDatasetProperties(DataSetPermId id, Map properties, + boolean overwrite) { + DataSetUpdate update = new DataSetUpdate(); + update.setDataSetId(id); + if (overwrite) { + update.setProperties(properties); + } else { + DataSetFetchOptions options = new DataSetFetchOptions(); + options.withProperties(); + DataSet oldDataset = openBIS.getDataSets(Arrays.asList(id), options).get(id); + for (String property : properties.keySet()) { + String newValue = properties.get(property); + String oldValue = oldDataset.getProperty(property); + if (oldValue == null || oldValue.isEmpty() || oldValue.equals(newValue)) { + update.setProperty(property, newValue); + } else { + update.setProperty(property, + oldValue + ", " + newValue);//TODO this can be changed to any other strategy + } + } + } + openBIS.updateDataSets(Arrays.asList(update)); + } + + public OpenbisExperimentWithDescendants getExperimentAndDataFromSample(String sampleID) { + SampleSearchCriteria criteria = new SampleSearchCriteria(); + criteria.withIdentifier().thatEquals(sampleID); + + DataSetFetchOptions dataSetFetchOptions = new DataSetFetchOptions(); + dataSetFetchOptions.withType(); + dataSetFetchOptions.withRegistrator(); + dataSetFetchOptions.withExperiment(); + dataSetFetchOptions.withSample(); + SampleFetchOptions fetchOptions = new SampleFetchOptions(); + fetchOptions.withProperties(); + fetchOptions.withType().withPropertyAssignments().withPropertyType(); + fetchOptions.withDataSetsUsing(dataSetFetchOptions); + + ExperimentFetchOptions expFetchOptions = new ExperimentFetchOptions(); + expFetchOptions.withType(); + expFetchOptions.withProject(); + expFetchOptions.withProperties(); + fetchOptions.withExperimentUsing(expFetchOptions); + + List samples = openBIS.searchSamples(criteria, fetchOptions).getObjects(); + Sample sample = samples.get(0); + + List datasets = new ArrayList<>(); + Map> datasetCodeToFiles = new HashMap<>(); + for (DataSet dataset : sample.getDataSets()) { + datasets.add(new DatasetWithProperties(dataset)); + datasetCodeToFiles.put(dataset.getPermId().getPermId(), getDatasetFiles(dataset)); + } + return new OpenbisExperimentWithDescendants(sample.getExperiment(), samples, datasets, + datasetCodeToFiles); + } + + public OpenbisExperimentWithDescendants getExperimentStructureFromDataset(String datasetID) { + DataSetSearchCriteria criteria = new DataSetSearchCriteria(); + criteria.withPermId().thatEquals(datasetID); + + SampleFetchOptions sampleFetchOptions = new SampleFetchOptions(); + sampleFetchOptions.withProperties(); + sampleFetchOptions.withType().withPropertyAssignments().withPropertyType(); + + ExperimentFetchOptions expFetchOptions = new ExperimentFetchOptions(); + expFetchOptions.withType(); + expFetchOptions.withProject(); + expFetchOptions.withProperties(); + + DataSetFetchOptions dataSetFetchOptions = new DataSetFetchOptions(); + dataSetFetchOptions.withType(); + dataSetFetchOptions.withRegistrator(); + dataSetFetchOptions.withSampleUsing(sampleFetchOptions); + dataSetFetchOptions.withExperimentUsing(expFetchOptions); + + DataSet dataset = openBIS.searchDataSets(criteria, dataSetFetchOptions).getObjects().get(0); + + List samples = new ArrayList<>(); + if(dataset.getSample() != null) { + samples.add(dataset.getSample()); + } + + List datasets = new ArrayList<>(); + Map> datasetCodeToFiles = new HashMap<>(); + datasets.add(new DatasetWithProperties(dataset)); + datasetCodeToFiles.put(dataset.getPermId().getPermId(), getDatasetFiles(dataset)); + + if(dataset.getExperiment() == null) { + System.err.println("No experiment found for dataset "+datasetID); + } + return new OpenbisExperimentWithDescendants(dataset.getExperiment(), samples, datasets, + datasetCodeToFiles); + } +} diff --git a/src/main/java/life/qbic/model/download/OutputPathFinder.java b/src/main/java/life/qbic/model/download/OutputPathFinder.java new file mode 100644 index 0000000..2dbb977 --- /dev/null +++ b/src/main/java/life/qbic/model/download/OutputPathFinder.java @@ -0,0 +1,83 @@ +package life.qbic.model.download; + +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Methods to determine the final path for the output directory. + * The requested data will be downloaded into this directory. + */ +public class OutputPathFinder { + + private static final Logger LOG = LogManager.getLogger(OutputPathFinder.class); + + /** + * @param path to be shortened + * @return path that has no parents (top directory) + */ + private static Path getTopDirectory(Path path) { + Path currentPath = Paths.get(path.toString()); + Path parentPath; + while (currentPath.getParent() != null) { + parentPath = currentPath.getParent(); + currentPath = parentPath; + } + return currentPath; + } + + /** + * @param possiblePath: string that could be an existing Path to a directory + * @return true if path exists, false otherwise + */ + private static boolean isPathValid(String possiblePath){ + Path path = Paths.get(possiblePath); + return Files.isDirectory(path); + } + + /** + * @param file to download + * @param conservePaths if true, directory structure will be conserved + * @return final path to file itself + */ + private static Path determineFinalPathFromDataset(DataSetFile file, Boolean conservePaths ) { + Path finalPath; + if (conservePaths) { + finalPath = Paths.get(file.getPath()); + // drop top parent directory name in the openBIS DSS (usually "/origin") + Path topDirectory = getTopDirectory(finalPath); + finalPath = topDirectory.relativize(finalPath); + } else { + finalPath = Paths.get(file.getPath()).getFileName(); + } + return finalPath; + } + + /** + * @param outputPath provided by user + * @param prefix sample code + * @param file to download + * @param conservePaths provided by user + * @return output directory path + */ + public static Path determineOutputDirectory(String outputPath, Path prefix, DataSetFile file, boolean conservePaths){ + Path filePath = determineFinalPathFromDataset(file, conservePaths); + String path = File.separator + prefix.toString() + File.separator + filePath.toString(); + Path finalPath = Paths.get(""); + if (outputPath != null && !outputPath.isEmpty()) { + if(isPathValid(outputPath)) { + finalPath = Paths.get(outputPath + path); + } else{ + LOG.error("The path you provided does not exist."); + System.exit(1); + } + } else { + finalPath = Paths.get(System.getProperty("user.dir") + path); + } + return finalPath; + } +} diff --git a/src/main/java/life/qbic/model/download/SEEKConnector.java b/src/main/java/life/qbic/model/download/SEEKConnector.java new file mode 100644 index 0000000..157fbf6 --- /dev/null +++ b/src/main/java/life/qbic/model/download/SEEKConnector.java @@ -0,0 +1,1026 @@ +package life.qbic.model.download; + +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import javax.xml.parsers.ParserConfigurationException; +import life.qbic.model.AssetInformation; +import life.qbic.model.OpenbisSeekTranslator; +import life.qbic.model.SampleInformation; +import life.qbic.model.isa.SeekStructure; +import life.qbic.model.isa.GenericSeekAsset; +import life.qbic.model.isa.ISAAssay; +import life.qbic.model.isa.ISASample; +import life.qbic.model.isa.ISASampleType; +import life.qbic.model.isa.ISAStudy; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.client.utils.URIBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.xml.sax.SAXException; + +public class SEEKConnector { + + private static final Logger LOG = LogManager.getLogger(SEEKConnector.class); + private String apiURL; + private byte[] credentials; + private OpenbisSeekTranslator translator; + private final String DEFAULT_PROJECT_ID; + private String currentStudy; + private final List ASSET_TYPES = new ArrayList<>(Arrays.asList("data_files", "models", + "sops", "documents", "publications")); + + public SEEKConnector(String seekURL, byte[] httpCredentials, String openBISBaseURL, + String defaultProjectTitle) throws URISyntaxException, IOException, + InterruptedException, ParserConfigurationException, SAXException { + this.apiURL = seekURL; + this.credentials = httpCredentials; + Optional projectID = getProjectWithTitle(defaultProjectTitle); + if (projectID.isEmpty()) { + throw new RuntimeException("Failed to find project with title: " + defaultProjectTitle + ". " + + "Please provide an existing default project."); + } + DEFAULT_PROJECT_ID = projectID.get(); + translator = new OpenbisSeekTranslator(openBISBaseURL, DEFAULT_PROJECT_ID); + } + + public void setDefaultInvestigation(String investigationTitle) + throws URISyntaxException, IOException, InterruptedException { + translator.setDefaultInvestigation(searchNodeWithTitle("investigations", + investigationTitle)); + } + + public void setDefaultStudy(String studyTitle) + throws URISyntaxException, IOException, InterruptedException { + this.currentStudy = searchNodeWithTitle("studies", studyTitle); + translator.setDefaultStudy(currentStudy); + } + + /** + * Lists projects and returns the optional identifier of the one matching the provided ID. + * Necessary because project search does not seem to work. + * @param projectTitle the title to search for + * @return + */ + private Optional getProjectWithTitle(String projectTitle) + throws IOException, InterruptedException, URISyntaxException { + String endpoint = apiURL+"/projects/"; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode hits = rootNode.path("data"); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + String id = hit.get("id").asText(); + String title = hit.get("attributes").get("title").asText(); + if(title.equals(projectTitle)) { + return Optional.of(id); + } + } + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + return Optional.empty(); + } + + public String addStudy(ISAStudy assay) + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/studies"; + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPOSTRequest(endpoint, assay.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=200) { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data").path("id"); + + return idNode.asText(); + } + + public String addAssay(ISAAssay assay) + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/assays"; + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPOSTRequest(endpoint, assay.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=200) { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data").path("id"); + + return idNode.asText(); + } + + public String createStudy(ISAStudy study) + throws IOException, URISyntaxException, InterruptedException, IOException { + String endpoint = apiURL+"/studies"; + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPOSTRequest(endpoint, study.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=200) { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data").path("id"); + + return idNode.asText(); + } + + private HttpRequest buildAuthorizedPATCHRequest(String endpoint, String body) throws URISyntaxException { + return HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .method("PATCH", HttpRequest.BodyPublishers.ofString(body)).build(); + } + + private HttpRequest buildAuthorizedPOSTRequest(String endpoint, String body) throws URISyntaxException { + return HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .POST(HttpRequest.BodyPublishers.ofString(body)).build(); + } + + public boolean studyExists(String id) throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/studies/"+id; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + return response.statusCode() == 200; + } + + public void printAttributeTypes() throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/sample_attribute_types"; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + System.err.println(response.body()); + } + + /* +-patient id should be linked somehow, maybe gender? + */ + + public void deleteSampleType(String id) throws URISyntaxException, IOException, + InterruptedException { + String endpoint = apiURL+"/sample_types"; + URIBuilder builder = new URIBuilder(endpoint); + builder.setParameter("id", id); + + HttpResponse response = HttpClient.newBuilder().build() + .send(HttpRequest.newBuilder().uri(builder.build()) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .DELETE().build(), BodyHandlers.ofString()); + + if(response.statusCode()!=201) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + public String createSampleType(ISASampleType sampleType) + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/sample_types"; + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPOSTRequest(endpoint, sampleType.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=201) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data").path("id"); + + return idNode.asText(); + } + + public String updateSample(ISASample isaSample, String sampleID) throws URISyntaxException, IOException, + InterruptedException { + String endpoint = apiURL+"/samples/"+sampleID; + isaSample.setSampleID(sampleID); + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPATCHRequest(endpoint, isaSample.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=200) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data").path("id"); + + return endpoint+"/"+idNode.asText(); + } + + public String createSample(ISASample isaSample) throws URISyntaxException, IOException, + InterruptedException { + String endpoint = apiURL+"/samples"; + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPOSTRequest(endpoint, isaSample.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=200) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data").path("id"); + + return endpoint+"/"+idNode.asText(); + } + + private AssetToUpload createAsset(String datasetCode, GenericSeekAsset data) + throws IOException, URISyntaxException, InterruptedException { + String endpoint = apiURL+"/"+data.getType(); + + HttpResponse response = HttpClient.newBuilder().build() + .send(buildAuthorizedPOSTRequest(endpoint, data.toJson()), + BodyHandlers.ofString()); + + if(response.statusCode()!=201 && response.statusCode()!=200) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + + JsonNode rootNode = new ObjectMapper().readTree(response.body()); + JsonNode idNode = rootNode.path("data") + .path("attributes") + .path("content_blobs") + .path(0).path("link"); + return new AssetToUpload(idNode.asText(), data.getFileName(), datasetCode, data.fileSizeInBytes()); + } + + public String uploadFileContent(String blobEndpoint, String file) + throws URISyntaxException, IOException, InterruptedException { + + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(blobEndpoint)) + .headers("Content-Type", "application/octet-stream") + .headers("Accept", "application/octet-stream") + .headers("Authorization", "Basic " + new String(credentials)) + .PUT(BodyPublishers.ofFile(new File(file).toPath())).build(); + + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + + if(response.statusCode()!=200) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + return blobEndpointToAssetURL(blobEndpoint); + } + + public String uploadStreamContent(String blobEndpoint, + Supplier streamSupplier) + throws URISyntaxException, IOException, InterruptedException { + + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(blobEndpoint)) + .headers("Content-Type", "application/octet-stream") + .headers("Accept", "*/*") + .headers("Authorization", "Basic " + new String(credentials)) + .PUT(BodyPublishers.ofInputStream(streamSupplier)).build(); + + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + + System.err.println("response was: "+response); + System.err.println("response body: "+response.body()); + + if(response.statusCode()!=200) { + System.err.println(response.body()); + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } else { + return blobEndpointToAssetURL(blobEndpoint); + } + } + + private String blobEndpointToAssetURL(String blobEndpoint) { + return blobEndpoint.split("content_blobs")[0]; + } + + public boolean endPointExists(String endpoint) + throws URISyntaxException, IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + return response.statusCode() == 200; + } + + /** + * Creates + * @param isaToOpenBISFile + * @param assays + * @return + * @throws IOException + * @throws URISyntaxException + * @throws InterruptedException + */ + public List createAssetsForAssays(Map isaToOpenBISFile, List assays) + throws IOException, URISyntaxException, InterruptedException { + List result = new ArrayList<>(); + for (GenericSeekAsset isaFile : isaToOpenBISFile.keySet()) { + if(!assays.isEmpty()) { + isaFile.withAssays(assays); + } + result.add(createAsset(isaToOpenBISFile.get(isaFile).getDataSetPermId().getPermId(), + isaFile)); + } + return result; + } + + public String listAssays() throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/assays/"; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + return response.body(); + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + public Map getSampleTypeNamesToIDs() + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/sample_types/"; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(endpoint)) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + return parseSampleTypesJSON(response.body()); + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + private Map parseSampleTypesJSON(String json) throws JsonProcessingException { + Map typesToIDs = new HashMap<>(); + JsonNode rootNode = new ObjectMapper().readTree(json); + JsonNode hits = rootNode.path("data"); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + String id = hit.get("id").asText(); + String title = hit.get("attributes").get("title").asText(); + typesToIDs.put(title, id); + } + return typesToIDs; + } + + public boolean sampleTypeExists(String typeCode) + throws URISyntaxException, IOException, InterruptedException { + JsonNode result = genericSearch("sample_types", typeCode); + JsonNode hits = result.path("data"); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + if (hit.get("attributes").get("title").asText().equals(typeCode)) { + return true; + } + } + return false; + } + + /** + * Performs a generic search and returns the response in JSON format + * @param nodeType the type of SEEK node to search for + * @param searchTerm the term to search for + * @return JsonNode of the server's response + */ + private JsonNode genericSearch(String nodeType, String searchTerm) + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/search"; + URIBuilder builder = new URIBuilder(endpoint); + builder.setParameter("q", searchTerm).setParameter("search_type", nodeType); + + HttpRequest request = HttpRequest.newBuilder() + .uri(builder.build()) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + return new ObjectMapper().readTree(response.body()); + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + private String searchNodeWithTitle(String nodeType, String title) + throws URISyntaxException, IOException, InterruptedException { + JsonNode result = genericSearch(nodeType, title); + JsonNode hits = result.path("data"); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + if (hit.get("attributes").get("title").asText().equals(title)) { + return hit.get("id").asText(); + } + } + throw new RuntimeException("Matching " + nodeType + " title was not found : " + title); + } + + /** + * Searches for assays containing a search term and returns a list of found assay ids + * @param searchTerm the search term that should be in the assay properties - e.g. an openBIS id + * @return + * @throws URISyntaxException + * @throws IOException + * @throws InterruptedException + */ + public List searchAssaysInStudyContainingKeyword(String searchTerm) + throws URISyntaxException, IOException, InterruptedException { + + JsonNode result = genericSearch("assays", "*"+searchTerm+"*"); + + JsonNode hits = result.path("data"); + List assayIDsInStudy = new ArrayList<>(); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + String assayID = hit.get("id").asText(); + JsonNode assayData = fetchAssayData(assayID).get("data"); + JsonNode relationships = assayData.get("relationships"); + String studyID = relationships.get("study").get("data").get("id").asText(); + if(studyID.equals(currentStudy)) { + assayIDsInStudy.add(assayID); + } + } + return assayIDsInStudy; + } + + /** + * Searches for samples containing a search term and returns a list of found sample ids + * @param searchTerm the search term that should be in the assay properties - e.g. an openBIS id + * @return + * @throws URISyntaxException + * @throws IOException + * @throws InterruptedException + */ + public List searchSamplesContainingKeyword(String searchTerm) + throws URISyntaxException, IOException, InterruptedException { + + JsonNode result = genericSearch("samples", "*"+searchTerm+"*"); + + JsonNode hits = result.path("data"); + List assayIDs = new ArrayList<>(); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + assayIDs.add(hit.get("id").asText()); + } + return assayIDs; + } + + + public List searchAssetsContainingKeyword(String searchTerm) + throws URISyntaxException, IOException, InterruptedException { + List assetIDs = new ArrayList<>(); + for(String type : ASSET_TYPES) { + JsonNode result = genericSearch(type, "*"+searchTerm+"*"); + + JsonNode hits = result.path("data"); + for (Iterator it = hits.elements(); it.hasNext(); ) { + JsonNode hit = it.next(); + assetIDs.add(hit.get("id").asText()); + } + } + return assetIDs; + } + + + /** + * Updates information of an existing assay, its samples and attached assets. Missing samples and + * assets are created, but nothing missing from the new structure is deleted from SEEK. + * + * @param nodeWithChildren the translated Seek structure as it should be once the update is done + * @param assayID the assay id of the existing assay, that should be compared to the new + * structure + * @return information necessary to make post registration updates in openBIS and upload missing + * data to newly created assets. In the case of the update use case, only newly created objects + * will be contained in the return object. + */ + public SeekStructurePostRegistrationInformation updateAssayNode(SeekStructure nodeWithChildren, + String assayID) throws URISyntaxException, IOException, InterruptedException { + JsonNode assayData = fetchAssayData(assayID).get("data"); + Map sampleInfos = collectSampleInformation(assayData); + + // compare samples + Map newSamplesWithReferences = nodeWithChildren.getSamplesWithOpenBISReference(); + + List samplesToCreate = new ArrayList<>(); + for (ISASample newSample : newSamplesWithReferences.keySet()) { + String openBisID = newSamplesWithReferences.get(newSample); + SampleInformation existingSample = sampleInfos.get(openBisID); + if (existingSample == null) { + samplesToCreate.add(newSample); + System.out.printf("%s not found in SEEK. It will be created.%n", openBisID); + } else { + Map newAttributes = newSample.fetchCopyOfAttributeMap(); + for (String key : newAttributes.keySet()) { + Object newValue = newAttributes.get(key); + Object oldValue = existingSample.getAttributes().get(key); + + boolean oldEmpty = oldValue == null || oldValue.toString().isEmpty(); + boolean newEmpty = newValue == null || newValue.toString().isEmpty(); + if ((!oldEmpty && !newEmpty) && !newValue.toString().equals(oldValue.toString())) { + System.out.printf("Mismatch found in %s attribute of %s. Sample will be updated.%n", + key, openBisID); + newSample.setAssayIDs(List.of(assayID)); + updateSample(newSample, existingSample.getSeekID()); + } + } + } + } + + // compare assets + Map assetInfos = collectAssetInformation(assayData); + Map newAssetsToFiles = nodeWithChildren.getISAFileToDatasetFiles(); + + List assetsToCreate = new ArrayList<>(); + for (GenericSeekAsset newAsset : newAssetsToFiles.keySet()) { + DataSetFile file = newAssetsToFiles.get(newAsset); + String newPermId = file.getDataSetPermId().getPermId(); + if (!assetInfos.containsKey(newPermId)) { + assetsToCreate.add(newAsset); + System.out.printf("Assets with Dataset PermId %s not found in SEEK. File %s from this " + + "Dataset will be created.%n", newPermId, newAsset.getFileName()); + } + } + Map sampleIDsWithEndpoints = new HashMap<>(); + for (ISASample sample : samplesToCreate) { + sample.setAssayIDs(Collections.singletonList(assayID)); + String sampleEndpoint = createSample(sample); + sampleIDsWithEndpoints.put(newSamplesWithReferences.get(sample), sampleEndpoint); + } + List assetsToUpload = new ArrayList<>(); + for (GenericSeekAsset asset : assetsToCreate) { + asset.withAssays(Collections.singletonList(assayID)); + assetsToUpload.add(createAsset(newAssetsToFiles.get(asset).getDataSetPermId().getPermId(), + asset)); + } + Map> datasetIDsWithEndpoints = new HashMap<>(); + + for (AssetToUpload asset : assetsToUpload) { + String endpointWithoutBlob = blobEndpointToAssetURL(asset.getBlobEndpoint()); + String dsCode = asset.getDataSetCode(); + if (datasetIDsWithEndpoints.containsKey(dsCode)) { + datasetIDsWithEndpoints.get(dsCode).add(endpointWithoutBlob); + } else { + datasetIDsWithEndpoints.put(dsCode, new HashSet<>( + List.of(endpointWithoutBlob))); + } + } + + String assayEndpoint = apiURL + "/assays/" + assayID; + + String expID = nodeWithChildren.getAssayWithOpenBISReference().getRight(); + Pair experimentIDWithEndpoint = new ImmutablePair<>(expID, assayEndpoint); + + SeekStructurePostRegistrationInformation postRegInfo = + new SeekStructurePostRegistrationInformation(assetsToUpload, sampleIDsWithEndpoints, + datasetIDsWithEndpoints); + postRegInfo.setExperimentIDWithEndpoint(experimentIDWithEndpoint); + return postRegInfo; + } + + private Map collectAssetInformation(JsonNode assayData) + throws URISyntaxException, IOException, InterruptedException { + Map assets = new HashMap<>(); + JsonNode relationships = assayData.get("relationships"); + for(String type : ASSET_TYPES) { + for (Iterator it = relationships.get(type).get("data").elements(); it.hasNext(); ) { + String assetID = it.next().get("id").asText(); + AssetInformation assetInfo = fetchAssetInformation(assetID, type); + if(assetInfo.getOpenbisPermId()!=null) { + assets.put(assetInfo.getOpenbisPermId(), assetInfo); + } else { + System.out.printf("No Dataset permID found for existing %s %s (id: %s)%n" + + "This asset will be treated as if it would not exist in the update.%n", + type, assetInfo.getTitle(), assetID); + } + } + } + return assets; + } + + private Map collectSampleInformation(JsonNode assayData) + throws URISyntaxException, IOException, InterruptedException { + Map samples = new HashMap<>(); + JsonNode relationships = assayData.get("relationships"); + for (Iterator it = relationships.get("samples").get("data").elements(); it.hasNext(); ) { + String sampleID = it.next().get("id").asText(); + SampleInformation info = fetchSampleInformation(sampleID); + samples.put(info.getOpenBisIdentifier(), info); + } + return samples; + } + + private AssetInformation fetchAssetInformation(String assetID, String assetType) + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/"+assetType+"/"+assetID; + URIBuilder builder = new URIBuilder(endpoint); + + HttpRequest request = HttpRequest.newBuilder() + .uri(builder.build()) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + JsonNode attributes = new ObjectMapper().readTree(response.body()).get("data").get("attributes"); + String title = attributes.get("title").asText(); + String description = attributes.get("description").asText(); + AssetInformation result = new AssetInformation(assetID, assetType, title, description); + Optional permID = tryParseDatasetPermID(title); + if(permID.isPresent()) { + result.setOpenbisPermId(permID.get()); + } else { + tryParseDatasetPermID(description).ifPresent(result::setOpenbisPermId); + } + return result; + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + private Optional tryParseDatasetPermID(String input) { + Matcher titleMatcher = OpenbisConnector.datasetCodePattern.matcher(input); + if(titleMatcher.find()) { + return Optional.of(titleMatcher.group()); + } + return Optional.empty(); + } + + public SeekStructurePostRegistrationInformation updateSampleNode(SeekStructure nodeWithChildren, + String sampleID) throws URISyntaxException, IOException, InterruptedException { + SampleInformation existingSampleInfo = fetchSampleInformation(sampleID); + //TODO to be able to connect samples with assets, we need to create a new assay, here + + // compare samples + Map newSamplesWithReferences = nodeWithChildren.getSamplesWithOpenBISReference(); + + List samplesToCreate = new ArrayList<>(); + for (ISASample newSample : newSamplesWithReferences.keySet()) { + String openBisID = newSamplesWithReferences.get(newSample); + if (!existingSampleInfo.getOpenBisIdentifier().equals(openBisID)) { + samplesToCreate.add(newSample); + System.out.printf("%s not found in SEEK. It will be created.%n", openBisID); + } else { + Map newAttributes = newSample.fetchCopyOfAttributeMap(); + for (String key : newAttributes.keySet()) { + Object newValue = newAttributes.get(key); + Object oldValue = existingSampleInfo.getAttributes().get(key); + + boolean oldEmpty = oldValue == null || oldValue.toString().isEmpty(); + boolean newEmpty = newValue == null || newValue.toString().isEmpty(); + if ((!oldEmpty && !newEmpty) && !newValue.equals(oldValue)) { + System.out.printf("Mismatch found in attributes of %s. Sample will be updated.%n", + openBisID); + updateSample(newSample, sampleID); + } + } + } + } + + // compare assets + Map newAssetsToFiles = nodeWithChildren.getISAFileToDatasetFiles(); + + //TODO follow creation of assets for assay, no way to be sure these are attached to similar samples + List assetsToCreate = new ArrayList<>(); + + Map sampleIDsWithEndpoints = new HashMap<>(); + for (ISASample sample : samplesToCreate) { + String sampleEndpoint = createSample(sample); + sampleIDsWithEndpoints.put(newSamplesWithReferences.get(sample), sampleEndpoint); + } + List assetsToUpload = new ArrayList<>(); + + for (GenericSeekAsset asset : assetsToCreate) { + assetsToUpload.add(createAsset(newAssetsToFiles.get(asset).getDataSetPermId().getPermId(), + asset)); + } + Map> datasetIDsWithEndpoints = new HashMap<>(); + + for (AssetToUpload asset : assetsToUpload) { + String endpointWithoutBlob = blobEndpointToAssetURL(asset.getBlobEndpoint()); + String dsCode = asset.getDataSetCode(); + if (datasetIDsWithEndpoints.containsKey(dsCode)) { + datasetIDsWithEndpoints.get(dsCode).add(endpointWithoutBlob); + } else { + datasetIDsWithEndpoints.put(dsCode, new HashSet<>( + List.of(endpointWithoutBlob))); + } + } + + return new SeekStructurePostRegistrationInformation(assetsToUpload, sampleIDsWithEndpoints, + datasetIDsWithEndpoints); + } + + private SampleInformation fetchSampleInformation(String sampleID) throws URISyntaxException, + IOException, InterruptedException { + String endpoint = apiURL+"/samples/"+sampleID; + URIBuilder builder = new URIBuilder(endpoint); + + HttpRequest request = HttpRequest.newBuilder() + .uri(builder.build()) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + JsonNode attributeNode = new ObjectMapper().readTree(response.body()).get("data").get("attributes"); + //title is openbis identifier - this is also added to attribute_map under the name: + //App.configProperties.get("seek_openbis_sample_title"); + String openBisId = attributeNode.get("title").asText(); + Map attributesMap = new ObjectMapper() + .convertValue(attributeNode.get("attribute_map"), Map.class); + return new SampleInformation(sampleID, openBisId, attributesMap); + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + private JsonNode fetchAssayData(String assayID) + throws URISyntaxException, IOException, InterruptedException { + String endpoint = apiURL+"/assays/"+assayID; + URIBuilder builder = new URIBuilder(endpoint); + + HttpRequest request = HttpRequest.newBuilder() + .uri(builder.build()) + .headers("Content-Type", "application/json") + .headers("Accept", "application/json") + .headers("Authorization", "Basic " + new String(credentials)) + .GET().build(); + HttpResponse response = HttpClient.newBuilder().build() + .send(request, BodyHandlers.ofString()); + if(response.statusCode() == 200) { + return new ObjectMapper().readTree(response.body()); + } else { + throw new RuntimeException("Failed : HTTP error code : " + response.statusCode()); + } + } + + public SeekStructurePostRegistrationInformation createNode(SeekStructure nodeWithChildren) + throws URISyntaxException, IOException, InterruptedException { + Pair assayIDPair = nodeWithChildren.getAssayWithOpenBISReference(); + + System.out.println("Creating assay..."); + String assayID = addAssay(assayIDPair.getKey()); + String assayEndpoint = apiURL+"/assays/"+assayID; + Pair experimentIDWithEndpoint = + new ImmutablePair<>(assayIDPair.getValue(), assayEndpoint); + + //wait for a bit, so we can be sure the assay that will be referenced by the samples has been created + Thread.sleep(3000); + + Map sampleIDsWithEndpoints = new HashMap<>(); + Map samplesWithReferences = nodeWithChildren.getSamplesWithOpenBISReference(); + if(!samplesWithReferences.isEmpty()) { + System.out.println("Creating samples..."); + } + for(ISASample sample : samplesWithReferences.keySet()) { + sample.setAssayIDs(Collections.singletonList(assayID)); + String sampleEndpoint = createSample(sample); + sampleIDsWithEndpoints.put(samplesWithReferences.get(sample), sampleEndpoint); + } + + Map isaToFileMap = nodeWithChildren.getISAFileToDatasetFiles(); + + if(!isaToFileMap.isEmpty()) { + System.out.println("Creating assets..."); + } + + List assetsToUpload = createAssetsForAssays(isaToFileMap, + Collections.singletonList(assayID)); + + Map> datasetIDsWithEndpoints = new HashMap<>(); + + for(AssetToUpload asset : assetsToUpload) { + String endpointWithoutBlob = blobEndpointToAssetURL(asset.getBlobEndpoint()); + String dsCode = asset.getDataSetCode(); + if(datasetIDsWithEndpoints.containsKey(dsCode)) { + datasetIDsWithEndpoints.get(dsCode).add(endpointWithoutBlob); + } else { + datasetIDsWithEndpoints.put(dsCode, new HashSet<>( + List.of(endpointWithoutBlob))); + } + } + SeekStructurePostRegistrationInformation postRegInfo = + new SeekStructurePostRegistrationInformation(assetsToUpload, sampleIDsWithEndpoints, + datasetIDsWithEndpoints); + postRegInfo.setExperimentIDWithEndpoint(experimentIDWithEndpoint); + return postRegInfo; + } + + /* + public SeekStructurePostRegistrationInformation createSampleWithAssets(SeekStructure nodeWithChildren) + throws URISyntaxException, IOException, InterruptedException { + Map sampleIDsWithEndpoints = new HashMap<>(); + Map samplesWithReferences = nodeWithChildren.getSamplesWithOpenBISReference(); + for(ISASample sample : samplesWithReferences.keySet()) { + String sampleEndpoint = createSample(sample); + sampleIDsWithEndpoints.put(samplesWithReferences.get(sample), sampleEndpoint); + } + + Map isaToFileMap = nodeWithChildren.getISAFileToDatasetFiles(); + + List assetsToUpload = createAssetsForAssays(isaToFileMap, new ArrayList<>()); + + Map> datasetIDsWithEndpoints = new HashMap<>(); + + for(AssetToUpload asset : assetsToUpload) { + String endpointWithoutBlob = blobEndpointToAssetURL(asset.getBlobEndpoint()); + String dsCode = asset.getDataSetCode(); + if(datasetIDsWithEndpoints.containsKey(dsCode)) { + datasetIDsWithEndpoints.get(dsCode).add(endpointWithoutBlob); + } else { + datasetIDsWithEndpoints.put(dsCode, new HashSet<>( + List.of(endpointWithoutBlob))); + } + } + return new SeekStructurePostRegistrationInformation(assetsToUpload, sampleIDsWithEndpoints, + datasetIDsWithEndpoints); + } + + public SeekStructurePostRegistrationInformation createStandaloneAssets( + Map isaToFileMap) + throws IOException, URISyntaxException, InterruptedException { + + List assetsToUpload = createAssetsForAssays(isaToFileMap, new ArrayList<>()); + + Map> datasetIDsWithEndpoints = new HashMap<>(); + + for(AssetToUpload asset : assetsToUpload) { + String endpointWithoutBlob = blobEndpointToAssetURL(asset.getBlobEndpoint()); + String dsCode = asset.getDataSetCode(); + if(datasetIDsWithEndpoints.containsKey(dsCode)) { + datasetIDsWithEndpoints.get(dsCode).add(endpointWithoutBlob); + } else { + datasetIDsWithEndpoints.put(dsCode, new HashSet<>( + List.of(endpointWithoutBlob))); + } + } + return new SeekStructurePostRegistrationInformation(assetsToUpload, datasetIDsWithEndpoints); + } + + */ + + public OpenbisSeekTranslator getTranslator() { + return translator; + } + + public static class AssetToUpload { + + private final String blobEndpoint; + private final String filePath; + private final String openBISDataSetCode; + private final long fileSizeInBytes; + + public AssetToUpload(String blobEndpoint, String filePath, String openBISDataSetCode, + long fileSizeInBytes) { + this.blobEndpoint = blobEndpoint; + this.filePath = filePath; + this.openBISDataSetCode = openBISDataSetCode; + this.fileSizeInBytes = fileSizeInBytes; + } + + public long getFileSizeInBytes() { + return fileSizeInBytes; + } + + public String getFilePath() { + return filePath; + } + + public String getBlobEndpoint() { + return blobEndpoint; + } + + public String getDataSetCode() { + return openBISDataSetCode; + } + } + + public class SeekStructurePostRegistrationInformation { + + private final List assetsToUpload; + private Optional> experimentIDWithEndpoint; + private final Map sampleIDsWithEndpoints; + private final Map> datasetIDsWithEndpoints; + + public SeekStructurePostRegistrationInformation(List assetsToUpload, + Map sampleIDsWithEndpoints, + Map> datasetIDsWithEndpoints) { + this.assetsToUpload = assetsToUpload; + this.sampleIDsWithEndpoints = sampleIDsWithEndpoints; + this.datasetIDsWithEndpoints = datasetIDsWithEndpoints; + this.experimentIDWithEndpoint = Optional.empty(); + } + + public SeekStructurePostRegistrationInformation(List assetsToUpload, + Map> datasetIDsWithEndpoints) { + this.sampleIDsWithEndpoints = new HashMap<>(); + this.datasetIDsWithEndpoints = datasetIDsWithEndpoints; + this.assetsToUpload = assetsToUpload; + this.experimentIDWithEndpoint = Optional.empty(); + } + + public void setExperimentIDWithEndpoint(Pair experimentIDWithEndpoint) { + this.experimentIDWithEndpoint = Optional.of(experimentIDWithEndpoint); + } + + public List getAssetsToUpload() { + return assetsToUpload; + } + + public Optional> getExperimentIDWithEndpoint() { + return experimentIDWithEndpoint; + } + + public Map getSampleIDsWithEndpoints() { + return sampleIDsWithEndpoints; + } + + public Map> getDatasetIDsWithEndpoints() { + return datasetIDsWithEndpoints; + } + + } +} diff --git a/src/main/java/life/qbic/model/download/SummaryWriter.java b/src/main/java/life/qbic/model/download/SummaryWriter.java new file mode 100644 index 0000000..dbf2438 --- /dev/null +++ b/src/main/java/life/qbic/model/download/SummaryWriter.java @@ -0,0 +1,9 @@ +package life.qbic.model.download; + +import java.io.IOException; +import java.util.List; + +public interface SummaryWriter { + + void reportSummary(List summary) throws IOException; +} diff --git a/src/main/java/life/qbic/model/isa/AbstractISAObject.java b/src/main/java/life/qbic/model/isa/AbstractISAObject.java new file mode 100644 index 0000000..c8f952c --- /dev/null +++ b/src/main/java/life/qbic/model/isa/AbstractISAObject.java @@ -0,0 +1,25 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * Used to create the outer "data" node of all SEEK json objects. + */ +public abstract class AbstractISAObject { + + public String toJson(SimpleModule module) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(this); + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{\"data\":"); + jsonBuilder.append(json); + jsonBuilder.append("}"); + return jsonBuilder.toString(); + } + +} diff --git a/src/main/java/life/qbic/model/isa/GenericSeekAsset.java b/src/main/java/life/qbic/model/isa/GenericSeekAsset.java new file mode 100644 index 0000000..28bff90 --- /dev/null +++ b/src/main/java/life/qbic/model/isa/GenericSeekAsset.java @@ -0,0 +1,255 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Model class for Seek assets. Contains all mandatory and some optional properties and attributes + * that are needed to create an asset in SEEK. The model and its getters (names) are structured in a + * way to enable the easy translation to JSON to use in SEEK queries. + * Mandatory parameters are found in the constructor, optional attributes can be set using + * withAttribute(attribute) notation. Since there are different types of assets, the isaType is a + * parameter here. + */ +public class GenericSeekAsset extends AbstractISAObject { + + private Attributes attributes; + private Relationships relationships; + private String assetType; + private long fileSizeInBytes; + + public GenericSeekAsset(String assetType, String title, String fileName, List projectIds, + long fileSizeInBytes) { + this.assetType = assetType; + this.attributes = new Attributes(title, fileName); + this.relationships = new Relationships(projectIds); + this.fileSizeInBytes = fileSizeInBytes; + } + + public long fileSizeInBytes() { + return fileSizeInBytes; + } + + public GenericSeekAsset withOtherCreators(String otherCreators) { + this.attributes.otherCreators = otherCreators; + return this; + } + + public GenericSeekAsset withAssays(List assays) { + this.relationships.assays = assays; + return this; + } + + public GenericSeekAsset withDataFormatAnnotations(List identifiers) { + this.attributes.withDataFormatAnnotations(identifiers); + return this; + } + + public String toJson() throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addSerializer(Relationships.class, new RelationshipsSerializer()); + return super.toJson(module); + } + + public String getType() { + return assetType; + } + + public Relationships getRelationships() { + return relationships; + } + + public Attributes getAttributes() { + return attributes; + } + + public String getFileName() { + return attributes.getContent_blobs().get(0).getOriginal_filename(); + } + + public void setDatasetLink(String dataSetLink, boolean transferDate) { + attributes.description = "This asset was imported from openBIS: "+dataSetLink; + if(!transferDate) { + attributes.setExternalLinkToData(dataSetLink); + } + } + + private class Relationships { + + private List projects; + private List assays; + + public Relationships(List projects) { + this.projects = projects; + this.assays = new ArrayList<>(); + } + + public List getProjects() { + return projects; + } + + public List getAssays() { + return assays; + } + } + + public class RelationshipsSerializer extends StdSerializer { + + public RelationshipsSerializer(Class t) { + super(t); + } + + public RelationshipsSerializer() { + this(null); + } + + @Override + public void serialize(Relationships relationships, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + + generateListJSON(jsonGenerator, "projects", relationships.projects, "projects"); + generateListJSON(jsonGenerator, "assays", relationships.assays, "assays"); + + jsonGenerator.writeEndObject(); + } + } + + private void generateListJSON(JsonGenerator generator, String name, List items, + String type) + throws IOException { + generator.writeObjectFieldStart(name); + generator.writeArrayFieldStart("data"); + for (String item : items) { + generator.writeStartObject(); + generator.writeStringField("id", item); + generator.writeStringField("type", type); + generator.writeEndObject(); + } + generator.writeEndArray(); + generator.writeEndObject(); + } + + private class Attributes { + + private String title; + private String description; + private List contentBlobs = new ArrayList<>(); + private String otherCreators = ""; + private List dataFormatAnnotations = new ArrayList<>(); + + public Attributes(String title, String fileName) { + this.title = title; + ContentBlob blob = new ContentBlob(fileName); + this.contentBlobs.add(blob); + } + + public void setExternalLinkToData(String dataSetLink) { + for(ContentBlob blob : contentBlobs) { + blob.setURL(dataSetLink); + } + } + + public String getDescription() { + return description; + } + + public String getTitle() { + return title; + } + + public List getContent_blobs() { + return contentBlobs; + } + + public String getOther_creators() { + return otherCreators; + } + + public List getData_format_annotations() { + return dataFormatAnnotations; + } + + public void withDataFormatAnnotations(List identifiers) { + List annotations = new ArrayList<>(); + for(String id : identifiers) { + annotations.add(new DataFormatAnnotation(id)); + } + this.dataFormatAnnotations = annotations; + } + + private class DataFormatAnnotation { + + private String label; + private String identifier; + + public DataFormatAnnotation(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + } + + private class ContentBlob { + + private String originalFilename; + private String contentType; + private String url; + + public ContentBlob(String fileName) { + this.originalFilename = fileName; + String suffix = fileName.substring(fileName.indexOf('.') + 1); + + this.contentType = "application/" + suffix; + } + + public String getContent_type() { + return contentType; + } + + public String getOriginal_filename() { + return originalFilename; + } + + public void setURL(String dataSetLink) { + this.url = dataSetLink; + } + + public String getUrl() { + return url; + } + } + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GenericSeekAsset)) { + return false; + } + + GenericSeekAsset that = (GenericSeekAsset) o; + return Objects.equals(attributes, that.attributes) && Objects.equals( + relationships, that.relationships) && Objects.equals(assetType, that.assetType); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attributes); + result = 31 * result + Objects.hashCode(relationships); + result = 31 * result + Objects.hashCode(assetType); + return result; + } +} diff --git a/src/main/java/life/qbic/model/isa/ISAAssay.java b/src/main/java/life/qbic/model/isa/ISAAssay.java new file mode 100644 index 0000000..b632b55 --- /dev/null +++ b/src/main/java/life/qbic/model/isa/ISAAssay.java @@ -0,0 +1,276 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Model class for ISA Assays. Contains all mandatory and some optional properties and attributes + * that are needed to create assays in SEEK. The model and its getters (names) are structured in a + * way to enable the easy translation to JSON to use in SEEK queries. + * Mandatory parameters are found in the constructor, optional attributes can be set using + * withAttribute(attribute) notation. + */ +public class ISAAssay extends AbstractISAObject { + + private final String ISA_TYPE = "assays"; + + private Attributes attributes; + private Relationships relationships; + private String title; + + public ISAAssay(String title, String studyId, String assayClass, URI assayType) { + this.title = title; + this.attributes = new Attributes(title, assayClass, assayType); + this.relationships = new Relationships(studyId); + } + + public ISAAssay withOtherCreators(String otherCreators) { + this.attributes.otherCreators = otherCreators; + return this; + } + + public ISAAssay withTags(List tags) { + this.attributes.tags = tags; + return this; + } + + public ISAAssay withDescription(String description) { + this.attributes.description = description; + return this; + } + + public ISAAssay withTechnologyType(String technologyType) { + this.attributes.technologyType = technologyType; + return this; + } + + public void setCreatorIDs(List creators) { + this.relationships.setCreatorIDs(creators); + } + public void setOrganismIDs(List organismIDs) { + this.relationships.setOrganismIDs(organismIDs); + } + public void setSampleIDs(List sampleIDs) { + this.relationships.setSampleIDs(sampleIDs); + } + public void setDataFileIDs(List dataFileIDs) { + this.relationships.setDataFileIDs(dataFileIDs); + } + public void setSOPIDs(List sopiDs) { + this.relationships.setSOPIDs(sopiDs); + } + public void setDocumentIDs(List documentIDs) { + this.relationships.setDocumentIDs(documentIDs); + } + + public String toJson() throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addSerializer(Relationships.class, new RelationshipsSerializer()); + return super.toJson(module); + } + + public String getType() { + return ISA_TYPE; + } + + public Relationships getRelationships() { + return relationships; + } + + public Attributes getAttributes() { + return attributes; + } + + private class Relationships { + + private String studyId; + private List creators = new ArrayList<>(); + private List samples = new ArrayList<>(); + private List documents = new ArrayList<>(); + private List dataFiles = new ArrayList<>(); + private List sops = new ArrayList<>(); + private List organisms = new ArrayList<>(); + + public Relationships(String studyId) { + this.studyId = studyId; + } + + public String getStudyId() { + return studyId; + } + + public List getCreators() { + return creators; + } + + public void setCreatorIDs(List creators) { + this.creators = creators; + } + public void setSampleIDs(List samples) { + this.samples = samples; + } + public void setDocumentIDs(List documents) { + this.documents = documents; + } + public void setDataFileIDs(List dataFiles) { + this.dataFiles = dataFiles; + } + public void setSOPIDs(List sops) { + this.sops = sops; + } + public void setOrganismIDs(List organisms) { + this.organisms = organisms; + } + } + + public class RelationshipsSerializer extends StdSerializer { + + public RelationshipsSerializer(Class t) { + super(t); + } + + public RelationshipsSerializer() { + this(null); + } + + @Override + public void serialize(Relationships relationships, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeObjectFieldStart("study"); + jsonGenerator.writeObjectFieldStart("data"); + jsonGenerator.writeStringField("id", relationships.getStudyId()); + jsonGenerator.writeStringField("type", "studies"); + jsonGenerator.writeEndObject(); + jsonGenerator.writeEndObject(); + generateListJSON(jsonGenerator, "creators", relationships.getCreators(), "people"); + generateListJSON(jsonGenerator, "samples", relationships.samples, "samples"); + generateListJSON(jsonGenerator, "documents", relationships.documents, "documents"); + generateListJSON(jsonGenerator, "data_files", relationships.dataFiles, "data_files"); + generateListJSON(jsonGenerator, "sops", relationships.sops, "sops"); + generateListJSON(jsonGenerator, "organisms", relationships.organisms, "organisms"); + + jsonGenerator.writeEndObject(); + } + } + + private void generateListJSON(JsonGenerator generator, String name, List items, String type) + throws IOException { + generator.writeObjectFieldStart(name); + generator.writeArrayFieldStart("data"); + for(int item : items) { + generator.writeStartObject(); + generator.writeStringField("id", Integer.toString(item)); + generator.writeStringField("type", type); + generator.writeEndObject(); + } + generator.writeEndArray(); + generator.writeEndObject(); + } + + protected class Attributes { + + public List tags = new ArrayList<>(); + public String description = ""; + public String technologyType = ""; + private String title; + private AssayClass assayClass; + private AssayType assayType; + private String otherCreators = ""; + + public Attributes(String title, String assayClass, URI assayType) { + this.title = title; + this.assayClass = new AssayClass(assayClass); + this.assayType = new AssayType(assayType.toString()); + } + + public List getTags() { + return tags; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getTechnologyType() { + return technologyType; + } + + public AssayClass getAssay_class() { + return assayClass; + } + + public AssayType getAssay_type() { + return assayType; + } + + public String getOther_creators() { + return otherCreators; + } + + public void withAssayType(String assayType) { + this.assayType = new AssayType(assayType); + } + + private class AssayClass { + + String key; + + public AssayClass(String assayClass) { + this.key = assayClass; + } + + public String getKey() { + return key; + } + } + + private class AssayType { + + String uri; + + public AssayType(String assayType) { + this.uri = assayType; + } + public String getUri() { + return uri; + } + } + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ISAAssay)) { + return false; + } + + ISAAssay isaAssay = (ISAAssay) o; + return Objects.equals(attributes, + isaAssay.attributes) && Objects.equals(relationships, isaAssay.relationships) + && Objects.equals(title, isaAssay.title); + } + + @Override + public int hashCode() { + int result = ISA_TYPE.hashCode(); + result = 31 * result + Objects.hashCode(attributes); + result = 31 * result + Objects.hashCode(relationships); + result = 31 * result + Objects.hashCode(title); + return result; + } +} diff --git a/src/main/java/life/qbic/model/isa/ISADataFile.java b/src/main/java/life/qbic/model/isa/ISADataFile.java new file mode 100644 index 0000000..7c0ddaf --- /dev/null +++ b/src/main/java/life/qbic/model/isa/ISADataFile.java @@ -0,0 +1,198 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Model class for ISA Data files. Contains all mandatory and some optional properties and attributes + * that are needed to create a data file in SEEK. The model and its getters (names) are structured in a + * way to enable the easy translation to JSON to use in SEEK queries. + * Mandatory parameters are found in the constructor, optional attributes can be set using + * withAttribute(attribute) notation. + */ +public class ISADataFile extends AbstractISAObject { + + private Attributes attributes; + private Relationships relationships; + private final String ISA_TYPE = "data_files"; + + public ISADataFile(String title, String fileName, List projectIds) { + this.attributes = new Attributes(title, fileName); + this.relationships = new Relationships(projectIds); + } + + public ISADataFile withOtherCreators(String otherCreators) { + this.attributes.otherCreators = otherCreators; + return this; + } + + public ISADataFile withAssays(List assays) { + this.relationships.assays = assays; + return this; + } + + public ISADataFile withDataFormatAnnotations(List identifiers) { + this.attributes.withDataFormatAnnotations(identifiers); + return this; + } + + public String toJson() throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addSerializer(Relationships.class, new RelationshipsSerializer()); + return super.toJson(module); + } + + public String getType() { + return ISA_TYPE; + } + + public Relationships getRelationships() { + return relationships; + } + + public Attributes getAttributes() { + return attributes; + } + + public String getFileName() { + return attributes.getContent_blobs().get(0).getOriginal_filename(); + } + + private class Relationships { + + private List projects; + private List assays; + + public Relationships(List projects) { + this.projects = projects; + this.assays = new ArrayList<>(); + } + + public List getProjects() { + return projects; + } + + public List getAssays() { + return assays; + } + } + + public class RelationshipsSerializer extends StdSerializer { + + public RelationshipsSerializer(Class t) { + super(t); + } + + public RelationshipsSerializer() { + this(null); + } + + @Override + public void serialize(Relationships relationships, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + + generateListJSON(jsonGenerator, "projects", relationships.projects, "projects"); + generateListJSON(jsonGenerator, "assays", relationships.assays, "assays"); + + jsonGenerator.writeEndObject(); + } + } + + private void generateListJSON(JsonGenerator generator, String name, List items, + String type) + throws IOException { + generator.writeObjectFieldStart(name); + generator.writeArrayFieldStart("data"); + for (String item : items) { + generator.writeStartObject(); + generator.writeStringField("id", item); + generator.writeStringField("type", type); + generator.writeEndObject(); + } + generator.writeEndArray(); + generator.writeEndObject(); + } + + private class Attributes { + + private String title; + private List contentBlobs = new ArrayList<>(); + private String otherCreators = ""; + private List dataFormatAnnotations = new ArrayList<>(); + + + public Attributes(String title, String fileName) { + this.title = title; + this.contentBlobs.add(new ContentBlob(fileName)); + } + + public String getTitle() { + return title; + } + + public List getContent_blobs() { + return contentBlobs; + } + + public String getOther_creators() { + return otherCreators; + } + + public List getData_format_annotations() { + return dataFormatAnnotations; + } + + public void withDataFormatAnnotations(List identifiers) { + List annotations = new ArrayList<>(); + for(String id : identifiers) { + annotations.add(new DataFormatAnnotation(id)); + } + this.dataFormatAnnotations = annotations; + } + + private class DataFormatAnnotation { + + private String label; + private String identifier; + + public DataFormatAnnotation(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + } + + private class ContentBlob { + + private String originalFilename; + private String contentType; + + public ContentBlob(String fileName) { + this.originalFilename = fileName; + String suffix = fileName.substring(fileName.indexOf('.') + 1); + + this.contentType = "application/" + suffix; + } + + public String getContent_type() { + return contentType; + } + + public String getOriginal_filename() { + return originalFilename; + } + } + } + +} diff --git a/src/main/java/life/qbic/model/isa/ISASample.java b/src/main/java/life/qbic/model/isa/ISASample.java new file mode 100644 index 0000000..b16e04f --- /dev/null +++ b/src/main/java/life/qbic/model/isa/ISASample.java @@ -0,0 +1,203 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Model class for ISA Samples. Contains all mandatory and some optional properties and attributes + * that are needed to create samples in SEEK. The model and its getters (names) are structured in a + * way to enable the easy translation to JSON to use in SEEK queries. + * Mandatory parameters are found in the constructor, optional attributes can be set using + * withAttribute(attribute) notation. + */ +public class ISASample extends AbstractISAObject { + + private Attributes attributes; + private Relationships relationships; + private final String ISA_TYPE = "samples"; + private String id; + + public ISASample(String title, Map attributeMap, String sampleTypeId, + List projectIds) { + this.attributes = new Attributes(title, attributeMap); + this.relationships = new Relationships(sampleTypeId, projectIds); + } + + public ISASample withOtherCreators(String otherCreators) { + this.attributes.otherCreators = otherCreators; + return this; + } + + public void setSampleID(String seekID) { + this.id = seekID; + } + + public void setCreatorIDs(List creatorIDs) { + this.relationships.setCreatorIDs(creatorIDs); + } + + public void setAssayIDs(List assayIDs) { + this.relationships.setAssayIDs(assayIDs); + } + + public String getId() { + return id; + } + + public String toJson() throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addSerializer(Relationships.class, new RelationshipsSerializer()); + return super.toJson(module); + } + + public String getType() { + return ISA_TYPE; + } + + public Relationships getRelationships() { + return relationships; + } + + public Attributes getAttributes() { + return attributes; + } + + public Map fetchCopyOfAttributeMap() { + return new HashMap<>(attributes.getAttribute_map()); + } + + private class Relationships { + + private String sampleTypeId; + private List projects; + private List creators = new ArrayList<>(); + private List assays = new ArrayList<>(); + + public Relationships(String sampleTypeId, List projects) { + this.projects = projects; + this.sampleTypeId = sampleTypeId; + } + + public String getSample_type() { + return sampleTypeId; + } + + public List getAssays() { + return assays; + } + + public void setAssayIDs(List assays) { + this.assays = assays; + } + + public List getProjects() { + return projects; + } + + public List getCreators() { + return creators; + } + + public void setCreatorIDs(List creators) { + this.creators = creators; + } + } + + public class RelationshipsSerializer extends StdSerializer { + + public RelationshipsSerializer(Class t) { + super(t); + } + + public RelationshipsSerializer() { + this(null); + } + + @Override + public void serialize(Relationships relationships, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeObjectFieldStart("sample_type"); + jsonGenerator.writeObjectFieldStart("data"); + jsonGenerator.writeStringField("id", relationships.sampleTypeId); + jsonGenerator.writeStringField("type", "sample_types"); + jsonGenerator.writeEndObject(); + jsonGenerator.writeEndObject(); + + generateListJSON(jsonGenerator, "creators", relationships.getCreators(), "people"); + generateListJSON(jsonGenerator, "projects", relationships.projects, "projects"); + generateListJSON(jsonGenerator, "assays", relationships.assays, "assays"); + + jsonGenerator.writeEndObject(); + } + } + + private void generateListJSON(JsonGenerator generator, String name, List items, String type) + throws IOException { + generator.writeObjectFieldStart(name); + generator.writeArrayFieldStart("data"); + for(String item : items) { + generator.writeStartObject(); + generator.writeStringField("id", item); + generator.writeStringField("type", type); + generator.writeEndObject(); + } + generator.writeEndArray(); + generator.writeEndObject(); + } + + private class Attributes { + + private Map attributeMap = new HashMap<>(); + private String otherCreators = ""; + private String title; + + public Attributes(String title, Map attributeMap) { + this.attributeMap = attributeMap; + this.title = title; + } + + public String getTitle() { + return title; + } + + public Map getAttribute_map() { + return attributeMap; + } + + public String getOther_creators() { + return otherCreators; + } + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ISASample)) { + return false; + } + + ISASample isaSample = (ISASample) o; + return Objects.equals(attributes, isaSample.attributes) && Objects.equals( + relationships, isaSample.relationships); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attributes); + result = 31 * result + Objects.hashCode(relationships); + result = 31 * result + ISA_TYPE.hashCode(); + return result; + } +} diff --git a/src/main/java/life/qbic/model/isa/ISASampleType.java b/src/main/java/life/qbic/model/isa/ISASampleType.java new file mode 100644 index 0000000..3ccee05 --- /dev/null +++ b/src/main/java/life/qbic/model/isa/ISASampleType.java @@ -0,0 +1,230 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Model class for SampleType. Contains all mandatory and some optional properties and attributes + * that are needed to create a sample type in SEEK. The model and its getters (names) are structured + * in a way to enable the easy translation to JSON to use in SEEK queries. + * Mandatory parameters are found in the constructor, optional attributes can be set using + * withAttribute(attribute) notation. + * Can be used to populate a SEEK installation with sample types taken from another system's API. + */ +public class ISASampleType extends AbstractISAObject { + + private Attributes attributes; + private Relationships relationships; + private final String ISA_TYPE = "sample_types"; + + public ISASampleType(String title, SampleAttribute titleAttribute, String projectID) { + this.attributes = new Attributes(title, titleAttribute); + this.relationships = new Relationships(Arrays.asList(projectID)); + } + + public void addSampleAttribute(String title, SampleAttributeType sampleAttributeType, + boolean required, String linkedSampleTypeIdOrNull) { + attributes.addSampleAttribute(title, sampleAttributeType, required, linkedSampleTypeIdOrNull); + } + + public ISASampleType withAssays(List assays) { + this.relationships.assays = assays; + return this; + } + + public String toJson() throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addSerializer(Relationships.class, new RelationshipsSerializer()); + return super.toJson(module); + } + + public String getType() { + return ISA_TYPE; + } + + public Relationships getRelationships() { + return relationships; + } + + public Attributes getAttributes() { + return attributes; + } + + private class Relationships { + + private List projects; + private List assays; + + public Relationships(List projects) { + this.projects = projects; + this.assays = new ArrayList<>(); + } + + public List getProjects() { + return projects; + } + + public List getAssays() { + return assays; + } + } + + public class RelationshipsSerializer extends StdSerializer { + + public RelationshipsSerializer(Class t) { + super(t); + } + + public RelationshipsSerializer() { + this(null); + } + + @Override + public void serialize(Relationships relationships, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + + generateListJSON(jsonGenerator, "projects", relationships.projects, "projects"); + generateListJSON(jsonGenerator, "assays", relationships.assays, "assays"); + + jsonGenerator.writeEndObject(); + } + } + + private void generateListJSON(JsonGenerator generator, String name, List items, + String type) + throws IOException { + generator.writeObjectFieldStart(name); + generator.writeArrayFieldStart("data"); + for (String item : items) { + generator.writeStartObject(); + generator.writeStringField("id", item); + generator.writeStringField("type", type); + generator.writeEndObject(); + } + generator.writeEndArray(); + generator.writeEndObject(); + } + + private class Attributes { + + private String title; + private List sampleAttributes = new ArrayList<>();; + + public Attributes(String title, SampleAttribute titleAttribute) { + this.title = title; + if(!titleAttribute.isTitle) { + throw new IllegalArgumentException("The first sample attribute must be the title attribute."); + } + this.sampleAttributes.add(titleAttribute); + } + + public void addSampleAttribute(String title, SampleAttributeType sampleAttributeType, + boolean required, String linkedSampleTypeIdOrNull) { + SampleAttribute sampleAttribute = new SampleAttribute(title, sampleAttributeType, false, + required).withLinkedSampleTypeId(linkedSampleTypeIdOrNull); + sampleAttributes.add(sampleAttribute); + } + + public String getTitle() { + return title; + } + + public List getSample_attributes() { + return sampleAttributes; + } + } + + public static class SampleAttribute { + + private String title; + private String description; + private SampleAttributeType sampleAttributeType; + private boolean isTitle; + private boolean required; + private String linkedSampleTypeId; + + public SampleAttribute(String title, SampleAttributeType sampleAttributeType, boolean isTitle, + boolean required) { + this.title = title; + this.isTitle = isTitle; + this.required = required; + this.sampleAttributeType = sampleAttributeType; + } + + public SampleAttribute withDescription(String description) { + this.description = description; + return this; + } + + public SampleAttribute withLinkedSampleTypeId(String linkedSampleTypeId) { + this.linkedSampleTypeId = linkedSampleTypeId; + return this; + } + + public SampleAttributeType getSample_attribute_type() { + return sampleAttributeType; + } + + public String getLinked_sample_type_id() { + return linkedSampleTypeId; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public boolean getRequired() { + return required; + } + + public boolean getIs_title() { + return isTitle; + } + } + + public static class SampleAttributeType { + private String id; + private String title; + private String baseType; + + public SampleAttributeType(String id, String title, String baseType) { + this.id = id; + this.title = title; + this.baseType = baseType; + } + + public String getBase_type() { + return baseType; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + @Override + public String toString() { + return "SampleAttributeType{" + + "id='" + id + '\'' + + ", title='" + title + '\'' + + ", baseType='" + baseType + '\'' + + '}'; + } + } + +} diff --git a/src/main/java/life/qbic/model/isa/ISAStudy.java b/src/main/java/life/qbic/model/isa/ISAStudy.java new file mode 100644 index 0000000..7a30d5a --- /dev/null +++ b/src/main/java/life/qbic/model/isa/ISAStudy.java @@ -0,0 +1,152 @@ +package life.qbic.model.isa; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Model class for ISA Studies. Contains all mandatory and some optional properties and attributes + * that are needed to create studies in SEEK. The model and its getters (names) are structured in a + * way to enable the easy translation to JSON to use in SEEK queries. + * Mandatory parameters are found in the constructor, optional attributes can be set using + * withAttribute(attribute) notation. + */ +public class ISAStudy extends AbstractISAObject { + + private Attributes attributes; + private Relationships relationships; + private final String ISA_TYPE = "studies"; + + public ISAStudy(String title, String investigationId) { + this.attributes = new Attributes(title); + this.relationships = new Relationships(investigationId); + } + + public ISAStudy withDescription(String description) { + this.attributes.description = description; + return this; + } + + public ISAStudy withExperimentalists(String experimentalists) { + this.attributes.experimentalists = experimentalists; + return this; + } + + public ISAStudy withOtherCreators(String otherCreators) { + this.attributes.otherCreators = otherCreators; + return this; + } + + public void setCreatorIDs(List creators) { + this.relationships.setCreatorIDs(creators); + } + + public String toJson() throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addSerializer(Relationships.class, new RelationshipsSerializer()); + return super.toJson(module); + } + + public String getType() { + return ISA_TYPE; + } + + public Relationships getRelationships() { + return relationships; + } + + public Attributes getAttributes() { + return attributes; + } + + private class Relationships { + + private String investigationId; + private List creators = new ArrayList<>(); + + public Relationships(String investigationId) { + this.investigationId = investigationId; + } + + public String getInvestigationId() { + return investigationId; + } + + public List getCreators() { + return creators; + } + + public void setCreatorIDs(List creators) { + this.creators = creators; + } + } + + public class RelationshipsSerializer extends StdSerializer { + + public RelationshipsSerializer(Class t) { + super(t); + } + + public RelationshipsSerializer() { + this(null); + } + + @Override + public void serialize(Relationships relationships, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeObjectFieldStart("investigation"); + jsonGenerator.writeObjectFieldStart("data"); + jsonGenerator.writeStringField("id", relationships.getInvestigationId()); + jsonGenerator.writeStringField("type", "investigations"); + jsonGenerator.writeEndObject(); + jsonGenerator.writeEndObject(); + jsonGenerator.writeObjectFieldStart("creators"); + jsonGenerator.writeArrayFieldStart("data"); + for(int personID : relationships.getCreators()) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeNumberField("id", personID); + jsonGenerator.writeStringField("type", "people"); + jsonGenerator.writeEndObject(); + } + jsonGenerator.writeEndArray(); + jsonGenerator.writeEndObject(); + jsonGenerator.writeEndObject(); + } + } + + private class Attributes { + + private String title; + private String description = ""; + private String experimentalists = ""; + private String otherCreators = ""; + + public Attributes(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getExperimentalists() { + return experimentalists; + } + + public String getOther_creators() { + return otherCreators; + } + } + +} diff --git a/src/main/java/life/qbic/model/isa/NodeType.java b/src/main/java/life/qbic/model/isa/NodeType.java new file mode 100644 index 0000000..e426dd3 --- /dev/null +++ b/src/main/java/life/qbic/model/isa/NodeType.java @@ -0,0 +1,5 @@ +package life.qbic.model.isa; + +public enum NodeType { + ASSAY, SAMPLE, ASSET +} diff --git a/src/main/java/life/qbic/model/isa/SeekStructure.java b/src/main/java/life/qbic/model/isa/SeekStructure.java new file mode 100644 index 0000000..b3826bf --- /dev/null +++ b/src/main/java/life/qbic/model/isa/SeekStructure.java @@ -0,0 +1,46 @@ +package life.qbic.model.isa; + +import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + +/** + * Stores newly created ISA objects for SEEK, as well as their respective openBIS reference. It is + * assumed that these references are Sample and Experiment Identifiers. PermIds of datasets are taken + * from stored DataSetFiles + */ +public class SeekStructure { + + private final Pair assayAndOpenBISReference; + private final Map samplesWithOpenBISReference; + private final Map isaToOpenBISFile; + + public SeekStructure(ISAAssay assay, String openBISReference) { + this.assayAndOpenBISReference = new ImmutablePair<>(assay, openBISReference); + this.samplesWithOpenBISReference = new HashMap<>(); + this.isaToOpenBISFile = new HashMap<>(); + } + + public void addSample(ISASample sample, String openBISReference) { + samplesWithOpenBISReference.put(sample, openBISReference); + } + + public void addAsset(GenericSeekAsset asset, DataSetFile file) { + isaToOpenBISFile.put(asset, file); + } + + public Pair getAssayWithOpenBISReference() { + return assayAndOpenBISReference; + } + + public Map getSamplesWithOpenBISReference() { + return samplesWithOpenBISReference; + } + + public Map getISAFileToDatasetFiles() { + return isaToOpenBISFile; + } + +} 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 + '\'' + + '}'; + } +} diff --git a/src/main/java/life/qbic/util/StringUtil.java b/src/main/java/life/qbic/util/StringUtil.java new file mode 100644 index 0000000..a2c8fa4 --- /dev/null +++ b/src/main/java/life/qbic/util/StringUtil.java @@ -0,0 +1,9 @@ +package life.qbic.util; + +public class StringUtil { + + public static boolean endsWithIgnoreCase(String input, String suffix) { + int suffixLength = suffix.length(); + return input.regionMatches(true, input.length() - suffixLength, suffix, 0, suffixLength); + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..750a990 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + +