From 122da0b856c92cc1625de85a680ad8193580c4ac Mon Sep 17 00:00:00 2001 From: Aldo Mihasi Date: Tue, 31 Jan 2023 13:58:43 +0200 Subject: [PATCH] initial commit --- .gitignore | 2 + Dockerfile | 18 + LICENSE.txt | 21 + README.md | 25 + THIRD-PARTY-NOTICES.txt | 429 ++++++++++++++++++ pom.xml | 123 +++++ settings.xml | 18 + .../ckanrepository/config/CkanConfig.java | 107 +++++ .../ckanrepository/config/ConfigLoader.java | 6 + .../config/ConfigLoaderImpl.java | 61 +++ .../interfaces/CkanDataset.java | 65 +++ .../interfaces/CkanDeposit.java | 194 ++++++++ src/main/resources/application.properties | 3 + src/main/resources/ckan.png | Bin 0 -> 14644 bytes 14 files changed, 1072 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 THIRD-PARTY-NOTICES.txt create mode 100644 pom.xml create mode 100644 settings.xml create mode 100644 src/main/java/eu/eudat/depositinterface/ckanrepository/config/CkanConfig.java create mode 100644 src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoader.java create mode 100644 src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoaderImpl.java create mode 100644 src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDataset.java create mode 100644 src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDeposit.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/ckan.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92322c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +target/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f30298 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +####################################### Build stage ####################################### +FROM maven:3.6.3-openjdk-11-slim AS build-stage + +COPY . /build/ +WORKDIR /build/ +RUN mvn clean package -DskipTests + +ARG CREPO_BINARIES_REPO_URL +ARG CREPO_BINARIES_CREDENTIAL +ARG BUILD_VERSION +ENV CREPO_BINARIES_REPO_URL=$CREPO_BINARIES_REPO_URL +ENV CREPO_BINARIES_CREDENTIAL=$CREPO_BINARIES_CREDENTIAL +ENV BUILD_VERSION=$BUILD_VERSION + +RUN curl --location --request PUT "${CREPO_BINARIES_REPO_URL}opendmp/repository-jars/ckan/ckan-deposit-${BUILD_VERSION}.jar" \ +--header "Authorization: Basic ${CREPO_BINARIES_CREDENTIAL}" \ +--header "Content-Type: application/json" \ +--data-binary "@/build/target/repositorydepositckan-1.0.0-SNAPSHOT.jar" \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..47b2a43 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 OpenAIRE AMKE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7bb73c --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Using Ckan repository with Argos + +The repository-deposit-ckan module implements the [https://code-repo.d4science.org/MaDgiK-CITE/repository-deposit-base](https://) interface for the Ckan repository +which has the DOI extension. + +## Setup + +After creating the jar from the project, environment variables should be set since they are used in the application.properties +1) STORAGE_TMP_CKAN - a temporary storage needed +2) CONFIGURATION_CKAN - path to json file which includes the configuration for the repository + +### JSON configuration file + +The following fields should be set:
+**depositType** - an integer representing how the dmp user can deposit in the repository,
+a. **0** stands for system deposition meaning the dmp is deposited using argos credentials to the repository,
+b. **1** stands for user deposition in which the argos user specifies his/her own credentials to the repository,
+c. **2** stands for both ways deposition if the repository allows the deposits of dmps to be made from both argos and users accounts
+note: depositType should be set to **0** since ckan does not provide oauth2 protocol but, instead, uses api tokens
+**repositoryId** - unique identifier for the repository
+**apiToken** - api token provided for the depositions
+**repositoryUrl** - repository's api url e.g. "http://localhost:5000/api/3/action/"
+**repositoryRecordUrl** - repository's record url, this url is used to index dmps that are created e.g. "http://localhost:5000/dataset?doi="
+**organization** - organization uuid (or name) in which all dmps that are deposited will be resided
+**hasLogo** - if the repository has a logo
\ No newline at end of file diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..0480858 --- /dev/null +++ b/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,429 @@ +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +This component uses third party material from the projects listed below. +The original copyright notice and the license under which CITE +received such third party material are set forth below. CITE +reserves all other rights not expressly granted, whether by +implication, estoppel or otherwise. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: reception@cite.gr + +1. spring-boot-starter-parent +2. spring-boot-starter-web +3. spring-boot-starter-webflux +4. json +5. repositorydepositbase + +spring-boot-starter-parent NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF spring-boot-starter-parent NOTICES, INFORMATION, AND LICENSE + +spring-boot-starter-web NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF spring-boot-starter-web NOTICES, INFORMATION, AND LICENSE \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6b7b2a3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.2 + + + + gr.cite.opendmp + repositorydepositckan + ${revision} + jar + + OpenDMP Repository Deposit Ckan + OpenDMP Repository Deposit Ckan + https://code-repo.d4science.org/MaDgiK-CITE/repository-deposit-ckan + + + MIT License + https://code-repo.d4science.org/MaDgiK-CITE/repository-deposit-ckan/src/branch/master/LICENSE.txt + repo + + + + + CITE S.A. + maven-central@cite.gr + CITE S.A. + https://www.cite.gr + + + + scm:git:git://code-repo.d4science.org + scm:git:ssh://code-repo.d4science.org + https://code-repo.d4science.org/MaDgiK-CITE/repository-deposit-ckan + + + + 1.0.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-webflux + + + + gr.cite.opendmp + repositorydepositbase + 1.0.1 + + + + org.json + json + 20160810 + + + + + + + maven-assembly-plugin + + + package + + single + + + + + + + eu.eudat.EuDatApplication + + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + *:* + + module-info.class + + + + + + org.json + ckanrepository.shaded.org.json + + + + + + + + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 0000000..e6874ab --- /dev/null +++ b/settings.xml @@ -0,0 +1,18 @@ + + + + ossrh + ${server_username} + ${server_password} + + + + + ossrh + + ${gpg_passphrase} + ${gpg_keyname} + + + + \ No newline at end of file diff --git a/src/main/java/eu/eudat/depositinterface/ckanrepository/config/CkanConfig.java b/src/main/java/eu/eudat/depositinterface/ckanrepository/config/CkanConfig.java new file mode 100644 index 0000000..9bda7f1 --- /dev/null +++ b/src/main/java/eu/eudat/depositinterface/ckanrepository/config/CkanConfig.java @@ -0,0 +1,107 @@ +package eu.eudat.depositinterface.ckanrepository.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import eu.eudat.depositinterface.repository.RepositoryDepositConfiguration; + +public class CkanConfig { + private enum DepositType { + SystemDeposit(0), UserDeposit(1), BothWaysDeposit(2); + + private final int value; + + DepositType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static DepositType fromInteger(int value) { + switch (value) { + case 0: + return SystemDeposit; + case 1: + return UserDeposit; + case 2: + return BothWaysDeposit; + default: + throw new RuntimeException("Unsupported Deposit Type"); + } + } + } + + @JsonProperty("depositType") + private int depositType; + @JsonProperty("repositoryId") + private String repositoryId; + @JsonProperty("apiToken") + private String apiToken; + @JsonProperty("repositoryUrl") + private String repositoryUrl; + @JsonProperty("repositoryRecordUrl") + private String repositoryRecordUrl; + @JsonProperty("organization") + private String organization; + @JsonProperty("hasLogo") + private boolean hasLogo; + + public int getDepositType() { + return depositType; + } + public void setDepositType(int depositType) { + this.depositType = depositType; + } + + public String getRepositoryId() { + return repositoryId; + } + public void setRepositoryId(String repositoryId) { + this.repositoryId = repositoryId; + } + + public String getApiToken() { + return apiToken; + } + public void setApiToken(String apiToken) { + this.apiToken = apiToken; + } + + public String getRepositoryUrl() { + return repositoryUrl; + } + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + public String getRepositoryRecordUrl() { + return repositoryRecordUrl; + } + public void setRepositoryRecordUrl(String repositoryRecordUrl) { + this.repositoryRecordUrl = repositoryRecordUrl; + } + + public String getOrganization() { + return organization; + } + public void setOrganization(String organization) { + this.organization = organization; + } + + public boolean isHasLogo() { + return hasLogo; + } + public void setHasLogo(boolean hasLogo) { + this.hasLogo = hasLogo; + } + + public RepositoryDepositConfiguration toRepoConfig() { + RepositoryDepositConfiguration config = new RepositoryDepositConfiguration(); + config.setDepositType(this.depositType); + config.setRepositoryId(this.repositoryId); + config.setRepositoryUrl(this.repositoryUrl); + config.setRepositoryRecordUrl(this.repositoryRecordUrl); + config.setHasLogo(this.hasLogo); + return config; + } +} diff --git a/src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoader.java b/src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoader.java new file mode 100644 index 0000000..26d084a --- /dev/null +++ b/src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoader.java @@ -0,0 +1,6 @@ +package eu.eudat.depositinterface.ckanrepository.config; + +public interface ConfigLoader { + byte[] getLogo(); + CkanConfig getCkanConfig(); +} diff --git a/src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoaderImpl.java b/src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoaderImpl.java new file mode 100644 index 0000000..675afe6 --- /dev/null +++ b/src/main/java/eu/eudat/depositinterface/ckanrepository/config/ConfigLoaderImpl.java @@ -0,0 +1,61 @@ +package eu.eudat.depositinterface.ckanrepository.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +@Service("ckanConfigLoader") +public class ConfigLoaderImpl implements ConfigLoader{ + private static final Logger logger = LoggerFactory.getLogger(ConfigLoaderImpl.class); + private static final ObjectMapper mapper = new ObjectMapper(); + + private CkanConfig ckanConfig; + + @Autowired + private Environment environment; + + @Override + public CkanConfig getCkanConfig() { + if(ckanConfig == null){ + try{ + ckanConfig = mapper.readValue(getStreamFromPath(environment.getProperty("configuration.ckan")), CkanConfig.class); + } catch (IOException e) { + logger.error(e.getLocalizedMessage(), e); + } + } + return ckanConfig; + } + + @Override + public byte[] getLogo() { + String logo = environment.getProperty("configuration.logo"); + if(logo != null && !logo.isEmpty()){ + InputStream logoStream = getStreamFromPath(logo); + try { + return logoStream.readAllBytes(); + } + catch (IOException e){ + logger.error(e.getMessage(), e); + return null; + } + } + return null; + } + + private InputStream getStreamFromPath(String filePath) { + try { + return new FileInputStream(filePath); + } catch (FileNotFoundException e) { + logger.info("loading from classpath"); + return getClass().getClassLoader().getResourceAsStream(filePath); + } + } +} diff --git a/src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDataset.java b/src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDataset.java new file mode 100644 index 0000000..999f1f6 --- /dev/null +++ b/src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDataset.java @@ -0,0 +1,65 @@ +package eu.eudat.depositinterface.ckanrepository.interfaces; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CkanDataset { + + private String name; + @JsonProperty("private") + private boolean isPrivate; + private String author; + private String author_email; + private String notes; + private String version; + private String owner_org; + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + public boolean isPrivate() { + return isPrivate; + } + public void setPrivate(boolean aPrivate) { + isPrivate = aPrivate; + } + + public String getAuthor() { + return author; + } + public void setAuthor(String author) { + this.author = author; + } + + public String getAuthor_email() { + return author_email; + } + public void setAuthor_email(String author_email) { + this.author_email = author_email; + } + + public String getNotes() { + return notes; + } + public void setNotes(String notes) { + this.notes = notes; + } + + public String getVersion() { + return version; + } + public void setVersion(String version) { + this.version = version; + } + + public String getOwner_org() { + return owner_org; + } + public void setOwner_org(String owner_org) { + this.owner_org = owner_org; + } + +} diff --git a/src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDeposit.java b/src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDeposit.java new file mode 100644 index 0000000..8b62584 --- /dev/null +++ b/src/main/java/eu/eudat/depositinterface/ckanrepository/interfaces/CkanDeposit.java @@ -0,0 +1,194 @@ +package eu.eudat.depositinterface.ckanrepository.interfaces; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.eudat.depositinterface.ckanrepository.config.CkanConfig; +import eu.eudat.depositinterface.ckanrepository.config.ConfigLoader; +import eu.eudat.depositinterface.models.DMPDepositModel; +import eu.eudat.depositinterface.repository.RepositoryDeposit; +import eu.eudat.depositinterface.repository.RepositoryDepositConfiguration; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.*; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.*; + +@Component +public class CkanDeposit implements RepositoryDeposit { + private static final Logger logger = LoggerFactory.getLogger(CkanDeposit.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private ConfigLoader configLoader; + private Environment environment; + + @Autowired + public CkanDeposit(ConfigLoader configLoader, Environment environment){ + this.configLoader = configLoader; + this.environment = environment; + } + + @Override + public String deposit(DMPDepositModel dmpDepositModel, String repositoryAccessToken) throws Exception { + + CkanConfig ckanConfig = this.configLoader.getCkanConfig(); + + String doi; + + CkanDataset dataset = new CkanDataset(); + dataset.setName(dmpDepositModel.getLabel().replaceAll("[^a-zA-Z0-9]+", "_").toLowerCase()); + //dataset.setPrivate(!dmpDepositModel.isPublic()); + dataset.setPrivate(true); + dataset.setNotes(dmpDepositModel.getDescription()); + dataset.setVersion(String.valueOf(dmpDepositModel.getVersion())); + dataset.setOwner_org(ckanConfig.getOrganization()); + dataset.setAuthor("Argos User"); + dataset.setAuthor_email("argosUser@example.com"); + + if(dmpDepositModel.getPreviousDOI() == null || dmpDepositModel.getPreviousDOI().isEmpty()){ + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", ckanConfig.getApiToken()); + headers.setContentType(MediaType.APPLICATION_JSON); + String url = ckanConfig.getRepositoryUrl() + "package_create"; + Object response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(dataset, headers), Object.class).getBody(); + Map respMap = objectMapper.convertValue(response, Map.class); + respMap = (Map) respMap.get("result"); + String id = String.valueOf(respMap.get("id")); + + doi = String.valueOf(respMap.get("doi")); + + this.uploadFiles(dmpDepositModel, id); + } + else{ + JsonNode datasetJson = this.getDatasetIdentifier(dmpDepositModel.getPreviousDOI()).get(0); + String datasetId = datasetJson.get("id").asText(); + String version = datasetJson.get("version").asText(); + + JsonNode files = datasetJson.get("resources"); + if(files.isArray()) { + for (JsonNode file : files) { + String fileId = file.get("id").asText(); + this.deleteFile(fileId); + } + } + + this.uploadFiles(dmpDepositModel, datasetId); + + Map resp = this.updateVersion(datasetId, version); + doi = (String) resp.get("doi"); + } + + return doi; + + } + + private Map updateVersion(String datasetId, String version){ + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", this.configLoader.getCkanConfig().getApiToken()); + headers.setContentType(MediaType.APPLICATION_JSON); + String serverUrl = this.configLoader.getCkanConfig().getRepositoryUrl() + "package_patch"; + Map body = new HashMap<>(); + body.put("id", datasetId); + body.put("version", String.valueOf(Integer.parseInt(version) + 1)); + RestTemplate restTemplate = new RestTemplate(); + return (Map)restTemplate.exchange(serverUrl, HttpMethod.POST, new HttpEntity<>(body, headers), Map.class).getBody().get("result"); + } + + private void deleteFile(String fileId){ + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", this.configLoader.getCkanConfig().getApiToken()); + headers.setContentType(MediaType.APPLICATION_JSON); + String serverUrl = this.configLoader.getCkanConfig().getRepositoryUrl() + "resource_delete"; + Map map = new HashMap<>(); + map.put("id", fileId); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.exchange(serverUrl, HttpMethod.POST, new HttpEntity<>(map, headers), Object.class); + } + + private void uploadFiles(DMPDepositModel dmpDepositModel, String id) throws IOException { + this.uploadFile(dmpDepositModel.getPdfFileName(), dmpDepositModel.getPdfFile(), id); + + String contentDisposition = dmpDepositModel.getRdaJson().getHeaders().get("Content-Disposition").get(0); + String jsonFileName = contentDisposition.substring(contentDisposition.lastIndexOf('=') + 1); + File rdaJson = new File(this.environment.getProperty("storage.temp") + jsonFileName); + OutputStream output = new FileOutputStream(rdaJson); + try { + output.write(Objects.requireNonNull(dmpDepositModel.getRdaJson().getBody())); + output.flush(); + output.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + this.uploadFile(jsonFileName, rdaJson, id); + Files.deleteIfExists(rdaJson.toPath()); + + if(dmpDepositModel.getSupportingFilesZip() != null) { + this.uploadFile(dmpDepositModel.getSupportingFilesZip().getName(), dmpDepositModel.getSupportingFilesZip(), id); + } + } + + private JsonNode getDatasetIdentifier(String previousDOI) throws JsonProcessingException { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", this.configLoader.getCkanConfig().getApiToken()); + String serverUrl = this.configLoader.getCkanConfig().getRepositoryUrl() + "package_search?q=doi:" + previousDOI + "&include_private=True"; + RestTemplate restTemplate = new RestTemplate(); + Object response = restTemplate.exchange(serverUrl, HttpMethod.GET, new HttpEntity<>(headers), Map.class).getBody().get("result"); + JsonNode jsonNode = objectMapper.readTree(new JSONObject((Map)response).toString()); + return jsonNode.findValues("results").get(0); + } + + private void uploadFile(String filename, File file, String datasetId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.set("Authorization", this.configLoader.getCkanConfig().getApiToken()); + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + Resource resource = new FileSystemResource(file); + multipartBodyBuilder.part("upload", resource) + .header("filename", filename); + multipartBodyBuilder.part("name", filename); + multipartBodyBuilder.part("package_id", datasetId); + MultiValueMap> multipartBody = multipartBodyBuilder.build(); + HttpEntity>> requestEntity = new HttpEntity<>(multipartBody, headers); + + String serverUrl = this.configLoader.getCkanConfig().getRepositoryUrl() + "resource_create"; + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity resp = restTemplate.postForEntity(serverUrl, requestEntity, Object.class); + } + + @Override + public RepositoryDepositConfiguration getConfiguration() { + eu.eudat.depositinterface.ckanrepository.config.CkanConfig ckanConfig = this.configLoader.getCkanConfig(); + return ckanConfig.toRepoConfig(); + } + + @Override + public String getLogo() { + RepositoryDepositConfiguration conf = this.getConfiguration(); + if(conf.isHasLogo()){ + byte[] logo = this.configLoader.getLogo(); + return (logo != null && logo.length != 0) ? Base64.getEncoder().encodeToString(logo) : null; + } + return null; + } + + @Override + public String authenticate(String code) { + return null; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..3a5e685 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,3 @@ +storage.temp=${STORAGE_TMP_CKAN} +configuration.ckan=${CONFIGURATION_CKAN} +configuration.ckan.logo=${CONFIGURATION_LOGO_CKAN} \ No newline at end of file diff --git a/src/main/resources/ckan.png b/src/main/resources/ckan.png new file mode 100644 index 0000000000000000000000000000000000000000..e01821e6e11bec3dd554a63214669a3f8f29c03e GIT binary patch literal 14644 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 wi;kTvCPxO~K}^e