From d4a366b51519d0203a593f93bc8e1616a0d685b0 Mon Sep 17 00:00:00 2001 From: Mark Patton Date: Fri, 22 Nov 2024 13:32:18 -0500 Subject: [PATCH] Add support for depositing using the DSpace REST API --- .../config/repository/DSpaceBinding.java | 38 ++++ .../config/repository/ProtocolBinding.java | 3 +- .../deposit/config/spring/DepositConfig.java | 1 - .../provider/dspace/DSpaceAssembler.java | 50 +++++ .../provider/dspace/DSpaceMetadataMapper.java | 200 ++++++++++++++++++ .../DeploymentTestDataService.java | 1 + .../DspaceDepositService.java | 112 ++++++++-- .../pass/deposit/transport/Transport.java | 3 +- .../transport/dspace/DSpaceSession.java | 97 +++++++++ .../transport/dspace/DSpaceTransport.java | 44 ++++ .../transport/dspace/DspaceResponse.java | 72 +++++++ .../src/main/resources/application.properties | 9 +- .../deposit/assembler/PassFileResourceIT.java | 2 +- .../AbstractJacksonMappingTest.java | 3 +- .../PropertyResolvingDeserializerTest.java | 4 +- .../RepositoryConfigMappingTest.java | 12 +- .../repository/SimpleClassMappingTest.java | 12 +- .../TransportConfigMappingTest.java | 8 +- .../spring/AwsParamStoreConfigTest.java | 4 + .../deposit/service/AbstractDepositIT.java | 2 - .../service/SubmissionProcessorIT.java | 50 ++++- .../DeploymentTestDataServiceIT.java | 6 +- .../support/jobs/ScheduledJobsTest.java | 4 +- .../resources/full-test-repositories.json | 13 +- .../messaging/status/DepositTaskIT.json | 6 +- .../submissions/sample1-unsubmitted.json | 70 +++--- .../resources/test-application.properties | 9 + 27 files changed, 747 insertions(+), 88 deletions(-) create mode 100644 pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/DSpaceBinding.java create mode 100644 pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceAssembler.java create mode 100644 pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceMetadataMapper.java rename pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/{deploymenttest => dspace}/DspaceDepositService.java (71%) create mode 100644 pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceSession.java create mode 100644 pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceTransport.java create mode 100644 pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DspaceResponse.java diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/DSpaceBinding.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/DSpaceBinding.java new file mode 100644 index 000000000..3e046bf62 --- /dev/null +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/DSpaceBinding.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Johns Hopkins University + * + * 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. + */ +package org.eclipse.pass.deposit.config.repository; + +import static org.eclipse.pass.deposit.transport.Transport.TRANSPORT_PROTOCOL; + +import java.util.Map; + +import org.eclipse.pass.deposit.transport.Transport; + +/** + * @author Russ Poetker (rpoetke1@jh.edu) + */ +public class DSpaceBinding extends ProtocolBinding { + static final String PROTO = "DSpace"; + + public DSpaceBinding() { + this.setProtocol(PROTO); + } + + @Override + public Map asPropertiesMap() { + return Map.of(TRANSPORT_PROTOCOL, Transport.PROTOCOL.DSpace.name()); + } +} diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/ProtocolBinding.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/ProtocolBinding.java index ef37888b4..7826bee3e 100644 --- a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/ProtocolBinding.java +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/repository/ProtocolBinding.java @@ -31,7 +31,8 @@ @JsonSubTypes.Type(value = SftpBinding.class, name = SftpBinding.PROTO), @JsonSubTypes.Type(value = SwordV2Binding.class, name = SwordV2Binding.PROTO), @JsonSubTypes.Type(value = FilesystemBinding.class, name = FilesystemBinding.PROTO), - @JsonSubTypes.Type(value = InvenioRdmBinding.class, name = InvenioRdmBinding.PROTO) + @JsonSubTypes.Type(value = InvenioRdmBinding.class, name = InvenioRdmBinding.PROTO), + @JsonSubTypes.Type(value = DSpaceBinding.class, name = DSpaceBinding.PROTO) }) public abstract class ProtocolBinding { diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/spring/DepositConfig.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/spring/DepositConfig.java index 85c4627d5..e6e885e8c 100644 --- a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/spring/DepositConfig.java +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/config/spring/DepositConfig.java @@ -223,5 +223,4 @@ DepositServiceErrorHandler errorHandler(CriticalRepositoryInteraction cri) { ExceptionHandlingThreadPoolExecutor executorService() { return new ExceptionHandlingThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(10)); } - } diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceAssembler.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceAssembler.java new file mode 100644 index 000000000..bfc5de92e --- /dev/null +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceAssembler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Johns Hopkins University + * + * 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. + */ +package org.eclipse.pass.deposit.provider.dspace; + +import static org.eclipse.pass.deposit.assembler.AssemblerSupport.buildMetadata; + +import java.util.List; +import java.util.Map; + +import org.eclipse.pass.deposit.assembler.AbstractAssembler; +import org.eclipse.pass.deposit.assembler.DepositFileResource; +import org.eclipse.pass.deposit.assembler.MetadataBuilder; +import org.eclipse.pass.deposit.assembler.MetadataBuilderFactory; +import org.eclipse.pass.deposit.assembler.PackageStream; +import org.eclipse.pass.deposit.assembler.ResourceBuilderFactory; +import org.eclipse.pass.deposit.assembler.SimplePackageStream; +import org.eclipse.pass.deposit.model.DepositSubmission; +import org.eclipse.pass.support.client.PassClient; +import org.springframework.stereotype.Component; + +@Component +public class DSpaceAssembler extends AbstractAssembler { + public DSpaceAssembler(MetadataBuilderFactory mbf, + ResourceBuilderFactory rbf, + PassClient passClient) { + super(mbf, rbf, passClient); + } + + @Override + protected PackageStream createPackageStream(DepositSubmission depositSubmission, + List custodialResources, + MetadataBuilder mb, ResourceBuilderFactory rbf, + Map options) { + buildMetadata(mb, options); + return new SimplePackageStream(depositSubmission, custodialResources, mb); + } +} diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceMetadataMapper.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceMetadataMapper.java new file mode 100644 index 000000000..eb9d3d713 --- /dev/null +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/provider/dspace/DSpaceMetadataMapper.java @@ -0,0 +1,200 @@ +/* + * Copyright 2024 Johns Hopkins University + * + * 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. + */ +package org.eclipse.pass.deposit.provider.dspace; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import org.eclipse.pass.deposit.model.DepositMetadata; +import org.eclipse.pass.deposit.model.DepositMetadata.Article; +import org.eclipse.pass.deposit.model.DepositMetadata.Journal; +import org.eclipse.pass.deposit.model.DepositMetadata.Manuscript; +import org.eclipse.pass.deposit.model.DepositMetadata.Person; +import org.eclipse.pass.deposit.model.DepositSubmission; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * dc.title for the Manuscript + * dc.publisher for the publisher name + * dc.identifier.citation for the Manuscript + * dc.identifier.doi for the DOI + * dc.contributor for each non-submitter associated with the Manuscript + * dc.description.abstract for the Manuscript + * dc.date.issued for the publication date + * DSPACE_FIELD_EMBARGO_LIFT Date that the embargo is lifted + * DSPACE_FIELD_EMBARGO_TERMS Date that the embargo is lifted + */ +@Component +public class DSpaceMetadataMapper { + @Value("${dspace.field.embargo.lift}") + private String dspaceFieldEmbargoLift; + + @Value("${dspace.field.embargo.terms}") + private String dspaceFieldEmbargoTerms; + + public String patchWorkspaceItem(DepositSubmission submission) { + DepositMetadata depositMd = submission.getMetadata(); + Journal journalMd = depositMd.getJournalMetadata(); + Article articleMd = depositMd.getArticleMetadata(); + Manuscript manuscriptMd = depositMd.getManuscriptMetadata(); + + JSONArray metadata = new JSONArray(); + + String title = manuscriptMd.getTitle(); + + // Required by DSpace + metadata.add(add_array("traditionalpageone", "dc.title", title)); + + if (journalMd != null && journalMd.getPublisherName() != null) { + metadata.add(add_array("traditionalpageone", "dc.publisher", journalMd.getPublisherName())); + } + + if (articleMd.getDoi() != null) { + metadata.add(add_array("traditionalpageone", "dc.identifier.doi", articleMd.getDoi().toString())); + } + + if (manuscriptMd.getMsAbstract() != null) { + metadata.add(add_array("traditionalpageone", "dc.description.abstract", manuscriptMd.getMsAbstract())); + } + + String citation = createCitation(submission); + + if (!citation.isEmpty()) { + metadata.add(add_array("traditionalpageone", "dc.identifier.citation", citation)); + } + + // TODO: Must format YYYY-MM-DD + // Required by DSpace + String publicationDate = journalMd.getPublicationDate(); + metadata.add(add_array("traditionalpageone", "dc.date.issued", publicationDate)); + + // Add non-submitters as authors + // TODO This is different from before + String[] authors = depositMd.getPersons().stream().filter( + p -> p.getType() != DepositMetadata.PERSON_TYPE.submitter). + map(Person::getName).toArray(String[]::new); + + metadata.add(add_array("traditionalpageone", "dc.contributor.author", authors)); + + ZonedDateTime embargoLiftDate = articleMd.getEmbargoLiftDate(); + + if (embargoLiftDate != null) { + String liftDate = embargoLiftDate.format(DateTimeFormatter.ISO_LOCAL_DATE); + + metadata.add(add_array("traditionalpageone", dspaceFieldEmbargoLift, liftDate)); + metadata.add(add_array("traditionalpageone", dspaceFieldEmbargoTerms, liftDate)); + } + + // Required by DSpace + metadata.add(add("license", "granted", "true")); + + return metadata.toString(); + } + + private JSONObject add(String section, String key, String value) { + JSONObject op = new JSONObject(); + + op.put("op", "add"); + op.put("path", "/sections/" + section + "/" + key); + op.put("value", value); + + return op; + } + + private JSONObject add_array(String section, String key, String... values) { + JSONObject op = new JSONObject(); + + op.put("op", "add"); + op.put("path", "/sections/" + section + "/" + key); + + JSONArray op_value = new JSONArray(); + for (String value : values) { + op_value.add(array_value(value)); + } + + op.put("value", op_value); + + return op; + } + + private JSONObject array_value(String value) { + JSONObject obj = new JSONObject(); + obj.put("value", value); + return obj; + } + + // TODO Could we use citation in CrossRef metadata? + private String createCitation(DepositSubmission submission) { + DepositMetadata submissionMd = submission.getMetadata(); + DepositMetadata.Article articleMd = submissionMd.getArticleMetadata(); + DepositMetadata.Journal journalMd = submissionMd.getJournalMetadata(); + + StringBuilder citationBldr = new StringBuilder(); + + int authorIndex = 0; + for (Person p: submissionMd.getPersons()) { + if (p.getType() == DepositMetadata.PERSON_TYPE.author) { + // Citation: For author 0, add name. For authorIndex 1 and 2, add comma then name. + // For author 3, add comma and "et al". For later authorIndex, do nothing. + if (authorIndex == 0) { + citationBldr.append(p.getReversedName()); + } else if (authorIndex <= 2) { + citationBldr.append(", " + p.getReversedName()); + } else if (authorIndex == 3) { + citationBldr.append(", et al"); + } + authorIndex++; + } + } + + // Add period at end of author list in citation + + if (citationBldr.length() > 0) { + citationBldr.append("."); + } + + // Attach a if not empty + // publication date - after a single space, in parens, followed by "." + if (journalMd != null && journalMd.getPublicationDate() != null && !journalMd.getPublicationDate().isEmpty()) { + citationBldr.append(" (" + journalMd.getPublicationDate() + ")."); + } + // article title - after single space, in double quotes with "." inside + if (articleMd != null && articleMd.getTitle() != null && !articleMd.getTitle().isEmpty()) { + citationBldr.append(" \"" + articleMd.getTitle() + ".\""); + } + // journal title - after single space, followed by "." + if (journalMd != null && journalMd.getJournalTitle() != null && !journalMd.getJournalTitle().isEmpty()) { + citationBldr.append(" " + journalMd.getJournalTitle() + "."); + } + // volume - after single space + if (articleMd != null && articleMd.getVolume() != null && !articleMd.getVolume().isEmpty()) { + citationBldr.append(" " + articleMd.getVolume()); + } + // issue - after single space, inside parens, followed by "." + if (articleMd != null && articleMd.getIssue() != null && !articleMd.getIssue().isEmpty()) { + citationBldr.append(" (" + articleMd.getIssue() + ")."); + } + // DOI - after single space, followed by "." + if (articleMd != null && articleMd.getDoi() != null) { + citationBldr.append(" " + articleMd.getDoi().toString() + "."); + } + + return citationBldr.toString(); + } +} diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataService.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataService.java index b0cded5d6..28cd0ae70 100644 --- a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataService.java +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataService.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Objects; +import org.eclipse.pass.deposit.support.dspace.DspaceDepositService; import org.eclipse.pass.support.client.ModelUtil; import org.eclipse.pass.support.client.PassClient; import org.eclipse.pass.support.client.PassClientSelector; diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DspaceDepositService.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/dspace/DspaceDepositService.java similarity index 71% rename from pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DspaceDepositService.java rename to pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/dspace/DspaceDepositService.java index 944779694..f6fc833f3 100644 --- a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/deploymenttest/DspaceDepositService.java +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/support/dspace/DspaceDepositService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.eclipse.pass.deposit.support.deploymenttest; +package org.eclipse.pass.deposit.support.dspace; import java.net.URI; import java.util.List; @@ -29,11 +29,11 @@ import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; +import org.eclipse.pass.deposit.assembler.DepositFileResource; import org.eclipse.pass.support.client.model.Deposit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -46,13 +46,18 @@ /** * @author Russ Poetker (rpoetke1@jh.edu) */ -@ConditionalOnProperty(name = "pass.test.data.job.enabled") @Service public class DspaceDepositService { private static final Logger LOG = LoggerFactory.getLogger(DspaceDepositService.class); private final RestClient restClient; + @Value("${dspace.api.url}") + private String dspaceApiUrl; + + @Value("${dspace.website.url}") + private String dspaceWebsiteUrl; + @Value("${dspace.user}") private String dspaceUsername; @@ -62,14 +67,12 @@ public class DspaceDepositService { @Value("${dspace.server}") private String dspaceServer; - @Value("${dspace.server.api.protocol}") - private String dspaceApiProtocol; + @Value("${dspace.collection.handle}") + private String dspaceCollectionHandle; - protected record AuthContext(String xsrfToken, String authToken){} + public record AuthContext(String xsrfToken, String authToken){} - public DspaceDepositService(@Value("${dspace.server.api.protocol}") String dspaceApiProtocol, - @Value("${dspace.server}") String dspaceServer, - @Value("${dspace.server.api.path}") String dspaceApiPath) { + public DspaceDepositService(@Value("${dspace.api.url}") String dspaceApiUrl) { PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() .setDefaultSocketConfig(SocketConfig.custom() .setSoTimeout(Timeout.ofMinutes(1)) @@ -84,9 +87,10 @@ public DspaceDepositService(@Value("${dspace.server.api.protocol}") String dspac .setConnectionManager(connectionManager) .disableCookieManagement() .build(); + this.restClient = RestClient.builder() .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) - .baseUrl(dspaceApiProtocol + "://" + dspaceServer + dspaceApiPath) + .baseUrl(dspaceApiUrl) .build(); } @@ -95,7 +99,7 @@ public DspaceDepositService(@Value("${dspace.server.api.protocol}") String dspac * authentication docs. * @return an AuthContext containing authToken and xsrfToken */ - AuthContext authenticate() { + public AuthContext authenticate() { // Using exchange is needed for this call because dspace returns 404, but the response headers has the // csrf token header DSPACE-XSRF-TOKEN ResponseEntity csrfResponse = restClient.get() @@ -123,7 +127,7 @@ AuthContext authenticate() { * Deletes the deposit in the remote repository. * @param deposit contains deposit info to do the delete */ - void deleteDeposit(Deposit deposit, AuthContext authContext) { + public void deleteDeposit(Deposit deposit, AuthContext authContext) { LOG.warn("Deleting Test Deposit In Dspace (PASS Deposit ID={})", deposit.getId()); URI accessUrl = deposit.getRepositoryCopy().getAccessUrl(); LOG.warn("Deposit accessUrl={}", accessUrl); @@ -155,12 +159,20 @@ private String getAuthHeaderValue(ResponseEntity response, String header) } private String parseHandleFilter(URI accessUrl) { - String handleDelim = dspaceApiProtocol + "://" + dspaceServer + "/handle/"; - String[] handleTokens = accessUrl.toString().split(handleDelim); - if (handleTokens.length != 2) { + String path = accessUrl.getPath(); + + String mark = "/handle/"; + int start = path.indexOf(mark); + + if (start == -1) { throw new RuntimeException("Unable to determine dspace item handle for " + accessUrl); } - return handleTokens[1]; + + return path.substring(start + mark.length()); + } + + public String createAccessUrlFromItemUuid(String itemUuid) { + return dspaceWebsiteUrl + "/items/" + itemUuid; } private String findItemUuid(String handleValue, AuthContext authContext, String submissionTitle) { @@ -226,4 +238,72 @@ private void deleteItem(String itemUuid, AuthContext authContext) { .toBodilessEntity(); LOG.warn("Deleted item UUID={}", itemUuid); } + + public String getUuidForHandle(String handle, AuthContext authContext) { + LOG.debug("Search Dspace for object with handle={}", handle); + + String searchResponse = restClient.get() + .uri("/discover/search/objects?query=handle:{handleValue}", handle) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", authContext.authToken()) + .retrieve() + .body(String.class); + + List> searchArray = JsonPath.parse(searchResponse).read("$..indexableObject[?(@.handle)]"); + + if (searchArray.size() == 1) { + Map itemMap = searchArray.get(0); + String uuid = itemMap.get("uuid").toString(); + + LOG.debug("Found object UUID={} with handle={}", uuid, handle); + + return uuid; + } + + throw new RuntimeException("Unable to find object with handle: " + handle); + } + + public String createWorkspaceItem(List files, AuthContext authContext) { + MultiValueMap body = new LinkedMultiValueMap<>(); + + for (DepositFileResource res: files) { + body.add("file", res.getResource()); + } + + String uuid = getUuidForHandle(dspaceCollectionHandle, authContext); + + String json = restClient.post() + .uri("/submission/workspaceitems?owningCollection={collectionUuid}", uuid) + .header("Authorization", authContext.authToken()) + .header("X-XSRF-TOKEN", authContext.xsrfToken()) + .header("Cookie", "DSPACE-XSRF-COOKIE=" + authContext.xsrfToken()) + .body(body) + .retrieve().body(String.class); + + return json; + } + + public void patchWorkspaceItem(int workspaceItemId, String patch, AuthContext authContext) { + restClient.patch() + .uri("/submission/workspaceitems/{workspaceItemId}", workspaceItemId) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", authContext.authToken()) + .header("X-XSRF-TOKEN", authContext.xsrfToken()) + .header("Cookie", "DSPACE-XSRF-COOKIE=" + authContext.xsrfToken()) + .body(patch) + .retrieve().toBodilessEntity(); + } + + public void createWorkflowItem(int workspaceItemId, AuthContext authContext) { + String workspaceItemUrl = dspaceApiUrl + "/submission/workspaceitems/" + workspaceItemId; + + restClient.post() + .uri("/workflow/workflowitems") + .header("Content-Type", "text/uri-list") + .header("Authorization", authContext.authToken()) + .header("X-XSRF-TOKEN", authContext.xsrfToken()) + .header("Cookie", "DSPACE-XSRF-COOKIE=" + authContext.xsrfToken()) + .body(workspaceItemUrl) + .retrieve().toBodilessEntity(); + } } diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/Transport.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/Transport.java index 3b569e529..222a3473b 100644 --- a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/Transport.java +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/Transport.java @@ -130,7 +130,8 @@ enum PROTOCOL { SWORDv2, filesystem, devnull, - invenioRdm + invenioRdm, + DSpace } /** diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceSession.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceSession.java new file mode 100644 index 000000000..456f4edf1 --- /dev/null +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceSession.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Johns Hopkins University + * + * 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. + */ +package org.eclipse.pass.deposit.transport.dspace; + +import java.util.Map; + +import com.jayway.jsonpath.JsonPath; +import org.eclipse.pass.deposit.assembler.PackageStream; +import org.eclipse.pass.deposit.model.DepositSubmission; +import org.eclipse.pass.deposit.provider.dspace.DSpaceMetadataMapper; +import org.eclipse.pass.deposit.support.dspace.DspaceDepositService; +import org.eclipse.pass.deposit.support.dspace.DspaceDepositService.AuthContext; +import org.eclipse.pass.deposit.transport.TransportResponse; +import org.eclipse.pass.deposit.transport.TransportSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * In order to do a deposit to DSpace, first a workspace item is created with the files. + * Then the workspace item is patched with the correct metadata. + * Finally a workflow item is created referencing the workspace item in order to trigger submission. + */ +class DSpaceSession implements TransportSession { + private static final Logger LOG = LoggerFactory.getLogger(DSpaceSession.class); + + private final DspaceDepositService dspaceDepositService; + private final DSpaceMetadataMapper dspaceMetadataMapper; + + public DSpaceSession(DspaceDepositService dspaceDepositService, DSpaceMetadataMapper dspaceMetadataMapper) { + this.dspaceDepositService = dspaceDepositService; + this.dspaceMetadataMapper = dspaceMetadataMapper; + } + + @Override + public TransportResponse send(PackageStream packageStream, Map metadata) { + try { + DepositSubmission depositSubmission = packageStream.getDepositSubmission(); + + LOG.warn("Processing Dspace Deposit for Submission: {}", depositSubmission.getId()); + + AuthContext authContext = dspaceDepositService.authenticate(); + + String patchJson = dspaceMetadataMapper.patchWorkspaceItem(depositSubmission); + String workspaceItemJson = dspaceDepositService.createWorkspaceItem( + packageStream.getCustodialContent(), authContext); + + LOG.debug("Created WorkspaceItem: {}", workspaceItemJson); + + // TODO Set workspace item id on Deposit.depositStatusRef so can check if it already exists. + // TODO Then check metadata to see if it needs to be patched. + + int workspaceItemId = JsonPath.parse(workspaceItemJson).read("$._embedded.workspaceitems[0].id"); + String itemUuid = JsonPath.parse(workspaceItemJson).read( + "$._embedded.workspaceitems[0]._embedded.item.uuid"); + + LOG.debug("Patching WorkspaceItem to add metadata {}", patchJson); + dspaceDepositService.patchWorkspaceItem(workspaceItemId, patchJson, authContext); + + LOG.debug("Creating WorkflowItem for WorkspaceItem {}", workspaceItemId); + dspaceDepositService.createWorkflowItem(workspaceItemId, authContext); + + String accessUrl = dspaceDepositService.createAccessUrlFromItemUuid(itemUuid); + + // TODO 422 indicates validation errors. Should mark the deposit as failed and not retry. + + LOG.warn("Completed DSpace Deposit for Submission: {}, accessUrl: {}", + depositSubmission.getId(), accessUrl); + return new DspaceResponse(true, accessUrl); + } catch (Exception e) { + LOG.error("Error depositing into DSpace", e); + return new DspaceResponse(false, null, e); + } + } + + @Override + public boolean closed() { + return true; + } + + @Override + public void close() { + // no-op resources are closed with try-with-resources + } +} diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceTransport.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceTransport.java new file mode 100644 index 000000000..5faa53f11 --- /dev/null +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DSpaceTransport.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Johns Hopkins University + * + * 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. + */ +package org.eclipse.pass.deposit.transport.dspace; + +import java.util.Map; + +import org.eclipse.pass.deposit.provider.dspace.DSpaceMetadataMapper; +import org.eclipse.pass.deposit.support.dspace.DspaceDepositService; +import org.eclipse.pass.deposit.transport.Transport; +import org.eclipse.pass.deposit.transport.TransportSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DSpaceTransport implements Transport { + @Autowired + private DspaceDepositService dspaceDepositService; + + @Autowired + private DSpaceMetadataMapper dspaceMetadataMapper; + + @Override + public PROTOCOL protocol() { + return PROTOCOL.DSpace; + } + + @Override + public TransportSession open(Map hints) { + return new DSpaceSession(dspaceDepositService, dspaceMetadataMapper); + } +} diff --git a/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DspaceResponse.java b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DspaceResponse.java new file mode 100644 index 000000000..55b903871 --- /dev/null +++ b/pass-deposit-services/deposit-core/src/main/java/org/eclipse/pass/deposit/transport/dspace/DspaceResponse.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 Johns Hopkins University + * + * 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. + */ +package org.eclipse.pass.deposit.transport.dspace; + +import java.io.IOException; +import java.net.URI; + +import org.eclipse.pass.deposit.service.DepositUtil; +import org.eclipse.pass.deposit.transport.TransportResponse; +import org.eclipse.pass.support.client.PassClient; +import org.eclipse.pass.support.client.model.CopyStatus; +import org.eclipse.pass.support.client.model.Deposit; +import org.eclipse.pass.support.client.model.DepositStatus; +import org.eclipse.pass.support.client.model.RepositoryCopy; + +/** + * @author Russ Poetker (rpoetke1@jh.edu) + */ +class DspaceResponse implements TransportResponse { + private final boolean success; + private final Throwable throwable; + private final String depositAccessUrl; + + DspaceResponse(boolean success, String depositAccessUrl) { + this(success, depositAccessUrl, null); + } + + DspaceResponse(boolean success, String depositAccessUrl, Throwable throwable) { + this.success = success; + this.depositAccessUrl = depositAccessUrl; + this.throwable = throwable; + } + + @Override + public boolean success() { + return success; + } + + @Override + public Throwable error() { + return throwable; + } + + @Override + public void onSuccess(DepositUtil.DepositWorkerContext depositWorkerContext, PassClient passClient) { + try { + RepositoryCopy repositoryCopy = depositWorkerContext.repoCopy(); + repositoryCopy.setAccessUrl(URI.create(depositAccessUrl)); + repositoryCopy.getExternalIds().add(depositAccessUrl); + repositoryCopy.setCopyStatus(CopyStatus.COMPLETE); + passClient.updateObject(repositoryCopy); + Deposit deposit = depositWorkerContext.deposit(); + deposit.setDepositStatus(DepositStatus.ACCEPTED); + passClient.updateObject(deposit); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/pass-deposit-services/deposit-core/src/main/resources/application.properties b/pass-deposit-services/deposit-core/src/main/resources/application.properties index 5445a2a79..8975c6938 100644 --- a/pass-deposit-services/deposit-core/src/main/resources/application.properties +++ b/pass-deposit-services/deposit-core/src/main/resources/application.properties @@ -25,13 +25,14 @@ pmc.ftp.port=${PMC_FTP_PORT} pmc.ftp.user=${PMC_FTP_USER} pmc.ftp.password=${PMC_FTP_PASSWORD} -dspace.host=${DSPACE_HOST} -dspace.port=${DSPACE_PORT} dspace.server=${DSPACE_SERVER} +dspace.api.url=${DSPACE_API_URL} +dspace.website.url=${DSPACE_WEBSITE_URL} dspace.user=${DSPACE_USER} dspace.password=${DSPACE_PASSWORD} -dspace.server.api.protocol=${DSPACE_API_PROTOCOL} -dspace.server.api.path=${DSPACE_API_PATH} +dspace.collection.handle=${DSPACE_COLLECTION_HANDLE} +dspace.field.embargo.lift=${DSPACE_FIELD_EMBARGO_LIFT:local.embargo.lift} +dspace.field.embargo.terms=${DSPACE_FIELD_EMBARGO_TERMS:local.embargo.terms} inveniordm.api.baseUrl=${INVENIORDM_API_BASE_URL:} inveniordm.api.verifySslCertificate=${INVENIORDM_VERIFY_SSL_CERT:true} diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/assembler/PassFileResourceIT.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/assembler/PassFileResourceIT.java index c654d4300..9ce0bd73e 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/assembler/PassFileResourceIT.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/assembler/PassFileResourceIT.java @@ -41,7 +41,7 @@ void testGetInputStream() throws IOException { file.setUri(fileUri); passClient.createObject(file); - PassFileResource passFileResource = new PassFileResource(passClient, file.getId()); + PassFileResource passFileResource = new PassFileResource(passClient, file.getId(), file.getName()); // WHEN InputStream inputStream = passFileResource.getInputStream(); diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/AbstractJacksonMappingTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/AbstractJacksonMappingTest.java index 5f5451561..6e14a9ec6 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/AbstractJacksonMappingTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/AbstractJacksonMappingTest.java @@ -30,8 +30,7 @@ @TestPropertySource( locations = "/test-application.properties", properties = { - "dspace.host=test-dspace-host", - "dspace.port=test-dspace-port", + "dspace.server=test-dspace-host:8000", "pmc.ftp.host=test-ftp-host", "pmc.ftp.port=test-ftp-port", }) diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/PropertyResolvingDeserializerTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/PropertyResolvingDeserializerTest.java index 76d5237f0..cbc00c20d 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/PropertyResolvingDeserializerTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/PropertyResolvingDeserializerTest.java @@ -30,7 +30,7 @@ public void noPropertyResolutionTest() throws Exception { RepositoryConfig.class); assertEquals(SwordV2Binding.PROTO, config.getTransportConfig().getProtocolBinding().getProtocol()); SwordV2Binding swordV2Binding = (SwordV2Binding) config.getTransportConfig().getProtocolBinding(); - assertTrue(swordV2Binding.getDefaultCollectionUrl().contains("http://test-dspace-host:test-dspace-port")); + assertTrue(swordV2Binding.getDefaultCollectionUrl().contains("http://test-dspace-host:8000")); } @Test @@ -39,6 +39,6 @@ public void resolvePropertiesTest() throws Exception { RepositoryConfig.class); assertTrue(config.getTransportConfig().getProtocolBinding().getProtocol().equals(SwordV2Binding.PROTO)); SwordV2Binding swordV2Binding = (SwordV2Binding) config.getTransportConfig().getProtocolBinding(); - assertFalse(swordV2Binding.getDefaultCollectionUrl().contains("${dspace.host}")); + assertFalse(swordV2Binding.getDefaultCollectionUrl().contains("${dspace.server}")); } } diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/RepositoryConfigMappingTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/RepositoryConfigMappingTest.java index a2184f7e8..ddaa547d9 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/RepositoryConfigMappingTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/RepositoryConfigMappingTest.java @@ -75,12 +75,12 @@ public class RepositoryConfigMappingTest extends AbstractJacksonMappingTest { " \"protocol\": \"SWORDv2\",\n" + " \"username\": \"sworduser\",\n" + " \"password\": \"swordpass\",\n" + - " \"server-fqdn\": \"${dspace.host}\",\n" + - " \"server-port\": \"${dspace.port}\",\n" + - " \"service-doc\": \"http://${dspace.host}:${dspace" + - ".port}/swordv2/servicedocument\",\n" + - " \"default-collection\": \"http://${dspace.host}:${dspace" + - ".port}/swordv2/collection/123456789/2\",\n" + + " \"server-fqdn\": null,\n" + + " \"server-port\": null,\n" + + " \"service-doc\": \"http://${dspace.server}" + + "/swordv2/servicedocument\",\n" + + " \"default-collection\": \"http://${dspace.server}" + + "/swordv2/collection/123456789/2\",\n" + " \"on-behalf-of\": null,\n" + " \"deposit-receipt\": true,\n" + " \"user-agent\": \"pass-deposit/x.y.z\"\n" + diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/SimpleClassMappingTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/SimpleClassMappingTest.java index d2e074306..171ca4b9e 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/SimpleClassMappingTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/SimpleClassMappingTest.java @@ -40,10 +40,10 @@ public class SimpleClassMappingTest extends AbstractJacksonMappingTest { " \"protocol\": \"SWORDv2\",\n" + " \"username\": \"sworduser\",\n" + " \"password\": \"swordpass\",\n" + - " \"service-doc\": \"http://${dspace.host}:${dspace" + - ".port}/swordv2/servicedocument\",\n" + - " \"default-collection\": \"http://${dspace" + - ".host}:${dspace.port}/swordv2/collection/123456789/2\",\n" + + " \"service-doc\": \"http://${dspace.server}" + + "/swordv2/servicedocument\",\n" + + " \"default-collection\": \"http://${dspace.server}" + + "/swordv2/collection/123456789/2\",\n" + " \"on-behalf-of\": null,\n" + " \"deposit-receipt\": true,\n" + " \"user-agent\": \"pass-deposit/x.y.z\"\n" + @@ -121,9 +121,9 @@ public void mapSwordBinding() throws IOException { assertEquals("SWORDv2", swordBinding.getProtocol()); assertEquals("sworduser", swordBinding.getUsername()); assertEquals("swordpass", swordBinding.getPassword()); - assertEquals("http://test-dspace-host:test-dspace-port/swordv2/servicedocument", + assertEquals("http://test-dspace-host:8000/swordv2/servicedocument", swordBinding.getServiceDocUrl()); - assertEquals("http://test-dspace-host:test-dspace-port/swordv2/collection/123456789/2", + assertEquals("http://test-dspace-host:8000/swordv2/collection/123456789/2", swordBinding.getDefaultCollectionUrl()); assertNull(swordBinding.getOnBehalfOf()); assertTrue(swordBinding.isDepositReceipt()); diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/TransportConfigMappingTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/TransportConfigMappingTest.java index 4c84073c2..b56e7984f 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/TransportConfigMappingTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/repository/TransportConfigMappingTest.java @@ -104,10 +104,10 @@ public class TransportConfigMappingTest extends AbstractJacksonMappingTest { " \"protocol\": \"SWORDv2\",\n" + " \"username\": \"sworduser\",\n" + " \"password\": \"swordpass\",\n" + - " \"service-doc\": \"http://${dspace.host}:${dspace" + - ".port}/swordv2/servicedocument\",\n" + - " \"default-collection\": \"http://${dspace" + - ".host}:${dspace.port}/swordv2/collection/123456789/2\",\n" + + " \"service-doc\": \"http://${dspace.server}" + + "/swordv2/servicedocument\",\n" + + " \"default-collection\": \"http://${dspace.server}" + + "/swordv2/collection/123456789/2\",\n" + " \"on-behalf-of\": null,\n" + " \"deposit-receipt\": true,\n" + " \"user-agent\": \"pass-deposit/x.y.z\",\n" + diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/spring/AwsParamStoreConfigTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/spring/AwsParamStoreConfigTest.java index 9c4bbd02d..3d8deb84d 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/spring/AwsParamStoreConfigTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/config/spring/AwsParamStoreConfigTest.java @@ -45,6 +45,10 @@ pass.client.password=${PASS_CORE_PASSWORD:test-pw} dspace.user=${DSPACE_USER:test@test.edu} dspace.password=${DSPACE_PASSWORD:test-dspace-pw} + dspace.server=localhost:8000 + dspace.api.url=http://localhost:8000/api + dspace.website.url=http://localhost:8000/website + dspace.collection.handle=1234/1 """ ) @ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class) diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/AbstractDepositIT.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/AbstractDepositIT.java index 78affd2c5..9b9db271c 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/AbstractDepositIT.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/AbstractDepositIT.java @@ -50,8 +50,6 @@ @TestPropertySource(properties = { "pass.deposit.repository.configuration=classpath:org/eclipse/pass/deposit/messaging/status/DepositTaskIT.json", - "dspace.host=localhost", - "dspace.port=9020", "dspace.user=test-dspace-user", "dspace.password=test-dspace-password", "dspace.server=localhost:9020", diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/SubmissionProcessorIT.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/SubmissionProcessorIT.java index 803c4954d..624b4fe82 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/SubmissionProcessorIT.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/service/SubmissionProcessorIT.java @@ -21,6 +21,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.patch; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.put; @@ -78,7 +79,11 @@ @TestPropertySource(properties = { "pass.deposit.repository.configuration=classpath:/full-test-repositories.json", "inveniordm.api.token=test-invenio-api-token", - "inveniordm.api.baseUrl=http://localhost:9030/api" + "inveniordm.api.baseUrl=http://localhost:9030/api", + "dspace.server=localhost:9030", + "dspace.api.url=http://localhost:9030/dspace/api", + "dspace.website.url=http://localhost:9030/dspace/website", + "dspace.collection.handle=collectionhandle" }) @WireMockTest(httpPort = 9030) public class SubmissionProcessorIT extends AbstractSubmissionIT { @@ -104,6 +109,7 @@ void testSubmissionProcessing_Full() throws Exception { ResourceTestUtil.readSubmissionJson("sample1-unsubmitted"))); resetGrantProjectName(submission, null); initInvenioApiStubs(); + initDSpaceApiStubs(); // WHEN/THEN testSubmissionProcessor(submission, false); @@ -120,6 +126,8 @@ void testSubmissionProcessing_Full_InvenioExistingRecord() throws Exception { ResourceTestUtil.readSubmissionJson("sample1-unsubmitted"))); resetGrantProjectName(submission, null); initInvenioApiStubs(); + initDSpaceApiStubs(); + String searchRecordsJsonResponse = "{ \"hits\": { \"hits\": [{ \"id\": \"existing-record-id\", " + "\"is_published\": \"false\"} ] } }"; stubFor(get("/api/user/records?q=metadata.title:%22Specific%20protein%20supplementation%20using%20" + @@ -149,7 +157,7 @@ void testSubmissionProcessing_SkipTestSubmission() throws Exception { // WHEN/THEN testSubmissionProcessor(submission, true); - verify(devNullTransport, times(4)).open(anyMap()); + verify(devNullTransport, times(5)).open(anyMap()); verify(filesystemTransport, times(0)).open(anyMap()); verify(invenioRdmTransport, times(0)).open(anyMap()); verifyInvenioApiStubs(0); @@ -162,6 +170,7 @@ void testSubmissionProcessing_DontSkipTestSubmission() throws Exception { ResourceTestUtil.readSubmissionJson("sample1-unsubmitted"))); resetGrantProjectName(submission, DeploymentTestDataService.PASS_E2E_TEST_GRANT); initInvenioApiStubs(); + initDSpaceApiStubs(); ReflectionTestUtils.setField(depositTaskHelper, "skipDeploymentTestDeposits", Boolean.FALSE); // WHEN/THEN @@ -236,7 +245,7 @@ private void testSubmissionProcessor(Submission submission, boolean usingDevNull List repoKeys = resultDeposits.stream() .map(deposit -> deposit.getRepository().getRepositoryKey()) .toList(); - List expectedRepoKey = List.of("PubMed Central", "JScholarship", "BagIt", "InvenioRDM"); + List expectedRepoKey = List.of("PubMed Central", "JScholarship", "BagIt", "InvenioRDM", "DSpace"); assertTrue(repoKeys.size() == expectedRepoKey.size() && repoKeys.containsAll(expectedRepoKey) && expectedRepoKey.containsAll(repoKeys)); Deposit pmcDeposit = resultDeposits.stream() @@ -365,6 +374,41 @@ private void initInvenioApiStubs() throws IOException { .willReturn(ok(publishJsonResponse))); } + private void initDSpaceApiStubs() throws IOException { + stubFor(get("/dspace/api/security/csrf").willReturn(WireMock.notFound(). + withHeader("DSPACE-XSRF-TOKEN", "csrftoken"))); + stubFor(post("/dspace/api/authn/login").willReturn(WireMock.ok().withHeader("Authorization", "authtoken"))); + + String searchJson = "{\n" + + " \"_embedded\": {\n" + + " \"searchResult\": {\n" + + " \"_embedded\": {\n" + + " \"objects\": [\n" + + " {\n" + + " \"_embedded\": {\n" + + " \"indexableObject\": {\n" + + " \"handle\": \"collectionhandle\",\n" + + " \"uuid\": \"collectionuuid\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + stubFor(get("/dspace/api/discover/search/objects?query=handle:collectionhandle") + .willReturn(ok(searchJson))); + + stubFor(post("/dspace/api/submission/workspaceitems?owningCollection=collectionuuid") + .willReturn(WireMock.ok("{\"_embedded\": {\"workspaceitems\": [{\"id\": 1," + + "\"_embedded\": {\"item\": {\"uuid\": \"uuid\"}}}]}}"))); + + stubFor(patch("/dspace/api/submission/workspaceitems/1").willReturn(WireMock.ok())); + + stubFor(post("/dspace/api/workflow/workflowitems").willReturn(WireMock.ok())); + } + private void verifyInvenioApiStubs(int expectedCount) throws IOException, URISyntaxException { WireMock.verify(expectedCount, getRequestedFor( urlEqualTo("/api/user/records?q=metadata.title:%22Specific%20protein%20supplementation%20using%20" + diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataServiceIT.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataServiceIT.java index 306e5299e..1e0eae1bf 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataServiceIT.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/deploymenttest/DeploymentTestDataServiceIT.java @@ -44,6 +44,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.eclipse.pass.deposit.service.AbstractDepositIT; +import org.eclipse.pass.deposit.support.dspace.DspaceDepositService; import org.eclipse.pass.support.client.PassClientSelector; import org.eclipse.pass.support.client.RSQL; import org.eclipse.pass.support.client.model.AwardStatus; @@ -73,8 +74,9 @@ * @author Russ Poetker (rpoetke1@jh.edu) */ @TestPropertySource(properties = { - "dspace.server.api.protocol=http", - "dspace.server.api.path=/server/api", + "dspace.server=localhost:9020", + "dspace.api.url=http://localhost:9020/server/api", + "dspace.website.url=http://localhost:9020/website", "pass.test.data.job.enabled=true", "pass.test.data.policy.title=test-policy-title", "pass.test.data.user.email=test-user-email@foo", diff --git a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/jobs/ScheduledJobsTest.java b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/jobs/ScheduledJobsTest.java index 235e03214..b13ded9b9 100644 --- a/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/jobs/ScheduledJobsTest.java +++ b/pass-deposit-services/deposit-core/src/test/java/org/eclipse/pass/deposit/support/jobs/ScheduledJobsTest.java @@ -45,9 +45,9 @@ "pass.deposit.jobs.3.init.delay=120", "dspace.user=test-dspace-user", "dspace.password=test-dspace-password", + "dspace.port=9020", "dspace.server=localhost:9020", - "dspace.server.api.protocol=http", - "dspace.server.api.path=/server/api", + "dspace.server.api.path=http://localhost/server/api", }) @DirtiesContext public class ScheduledJobsTest { diff --git a/pass-deposit-services/deposit-core/src/test/resources/full-test-repositories.json b/pass-deposit-services/deposit-core/src/test/resources/full-test-repositories.json index 19107b5f8..401ccd187 100644 --- a/pass-deposit-services/deposit-core/src/test/resources/full-test-repositories.json +++ b/pass-deposit-services/deposit-core/src/test/resources/full-test-repositories.json @@ -145,5 +145,16 @@ "protocol": "invenioRdm" } } + }, + "DSpace": { + "assembler": { + "specification": "DSpace", + "beanName": "DSpaceAssembler" + }, + "transport-config": { + "protocol-binding": { + "protocol": "DSpace" + } + } } -} \ No newline at end of file +} diff --git a/pass-deposit-services/deposit-core/src/test/resources/org/eclipse/pass/deposit/messaging/status/DepositTaskIT.json b/pass-deposit-services/deposit-core/src/test/resources/org/eclipse/pass/deposit/messaging/status/DepositTaskIT.json index 4fef904e9..fdcf51a29 100644 --- a/pass-deposit-services/deposit-core/src/test/resources/org/eclipse/pass/deposit/messaging/status/DepositTaskIT.json +++ b/pass-deposit-services/deposit-core/src/test/resources/org/eclipse/pass/deposit/messaging/status/DepositTaskIT.json @@ -27,8 +27,8 @@ "protocol": "SWORDv2", "username": "${dspace.user}", "password": "${dspace.password}", - "server-fqdn": "${dspace.host}", - "server-port": "${dspace.port}", + "server-fqdn": null, + "server-port": null, "service-doc": "${dspace.baseuri}/swordv2/servicedocument", "default-collection": "${dspace.baseuri}/swordv2/collection/${dspace.collection.handle}", "on-behalf-of": null, @@ -37,4 +37,4 @@ } } } -} \ No newline at end of file +} diff --git a/pass-deposit-services/deposit-core/src/test/resources/submissions/sample1-unsubmitted.json b/pass-deposit-services/deposit-core/src/test/resources/submissions/sample1-unsubmitted.json index 192e2f2e4..85de620c6 100644 --- a/pass-deposit-services/deposit-core/src/test/resources/submissions/sample1-unsubmitted.json +++ b/pass-deposit-services/deposit-core/src/test/resources/submissions/sample1-unsubmitted.json @@ -95,6 +95,13 @@ "id": "7", "@type": "Repository" }, + { + "name": "DSpace Repo", + "repositoryKey": "DSpace", + "description": "DSpace Repository", + "id": "8", + "@type": "Repository" + }, { "journalName": "AAPS PharmSci", "issns": [ @@ -104,7 +111,7 @@ "nlmta": "AAPS PharmSci", "pmcParticipation": "A", "publisher": null, - "id": "8", + "id": "9", "@type": "Journal" }, { @@ -112,10 +119,10 @@ "abstract": "This is a great paper!", "doi": "abcdef", "pmid": "fedcba", - "journal": "8", + "journal": "9", "volume": "123", "issue": "May 2015", - "id": "9", + "id": "10", "@type": "Publication" }, { @@ -127,23 +134,23 @@ { "id": "7" } ], "institution": "fake:institution1", - "id": "10", + "id": "11", "@type": "Policy" }, { "name": "National Eye Institute", "url": "http://example.com/eyeguys", "localKey": "aabbcc", - "policy": "10", - "id": "11", + "policy": "11", + "id": "12", "@type": "Funder" }, { "name": "International Eye Institute", "url": "http://example.com/othereyeguys", "localKey": "ddeeff", - "policy": "10", - "id": "12", + "policy": "11", + "id": "13", "@type": "Funder" }, { @@ -154,14 +161,14 @@ "awardDate": "2017-06-01T00:00:00.000Z", "startDate": "2017-05-01T00:00:00.000Z", "endDate": "2018-06-01T00:00:00.000Z", - "primaryFunder": "11", - "directFunder": "12", + "primaryFunder": "12", + "directFunder": "13", "pi": "2", "coPis": [ "3", "1" ], - "id": "13", + "id": "14", "@type": "Grant" }, { @@ -171,18 +178,19 @@ "submittedDate": "2017-06-02T00:00:00.000Z", "aggregatedDepositStatus": "NOT_STARTED", "submissionStatus": "APPROVAL_REQUESTED", - "publication": "9", + "publication": "10", "repositories": [ "4", "5", "6", - "7" + "7", + "8" ], "submitter": "2", "grants": [ - "13" + "14" ], - "id": "14", + "id": "15", "@type": "Submission" }, { @@ -191,8 +199,8 @@ "description": "Custodial content", "fileRole": "SUPPLEMENTAL", "mimeType": "image/jpg", - "submission": "14", - "id": "15", + "submission": "15", + "id": "16", "@type": "File" }, { @@ -201,8 +209,8 @@ "description": "Custodial content", "fileRole": "FIGURE", "mimeType": "image/tiff", - "submission": "14", - "id": "16", + "submission": "15", + "id": "17", "@type": "File" }, { @@ -211,8 +219,8 @@ "description": "Custodial content", "fileRole": "FIGURE", "mimeType": "image/png", - "submission": "14", - "id": "17", + "submission": "15", + "id": "18", "@type": "File" }, { @@ -221,8 +229,8 @@ "description": "Custodial content", "fileRole": "MANUSCRIPT", "mimeType": "application/msword", - "submission": "14", - "id": "18", + "submission": "15", + "id": "19", "@type": "File" }, { @@ -231,8 +239,8 @@ "description": "Custodial content", "fileRole": "TABLE", "mimeType": "application/vnd.oasis.opendocument.spreadsheet", - "submission": "14", - "id": "19", + "submission": "15", + "id": "20", "@type": "File" }, { @@ -241,8 +249,8 @@ "description": "Custodial content, meant to conflict with NIH package spec", "fileRole": "SUPPLEMENTAL", "mimeType": "application/xml", - "submission": "14", - "id": "20", + "submission": "15", + "id": "21", "@type": "File" }, { @@ -251,8 +259,8 @@ "description": "Custodial content, meant to conflict with NIH package spec", "fileRole": "SUPPLEMENTAL", "mimeType": "text/plain", - "submission": "14", - "id": "21", + "submission": "15", + "id": "22", "@type": "File" }, { @@ -261,8 +269,8 @@ "description": "Custodial content with a space in the filename", "fileRole": "SUPPLEMENTAL", "mimeType": "text/plain", - "submission": "14", - "id": "22", + "submission": "15", + "id": "23", "@type": "File" } ] diff --git a/pass-deposit-services/deposit-core/src/test/resources/test-application.properties b/pass-deposit-services/deposit-core/src/test/resources/test-application.properties index fcffe7631..fd0a40b38 100644 --- a/pass-deposit-services/deposit-core/src/test/resources/test-application.properties +++ b/pass-deposit-services/deposit-core/src/test/resources/test-application.properties @@ -20,3 +20,12 @@ spring.cloud.aws.s3.enabled=false pass.client.url=http://localhost:8080/ pass.client.user=test pass.client.password=test + +dspace.server=localhost:8000 +dspace.user=user +dspace.password=test +dspace.api.url=http://localhost:8000/api +dspace.website.url=http://localhost:8000/website +dspace.collection.handle=1234/1 +dspace.field.embargo.lift=local.embargo.lift +dspace.field.embargo.terms=local.embargo.terms