diff --git a/.codecov.yml b/.codecov.yml index 326dd3e0b2..a628d33cbe 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,6 +4,13 @@ # Can be validated via instructions at: # https://docs.codecov.io/docs/codecov-yaml#validate-your-repository-yaml +# Tell Codecov not to send a coverage notification until (at least) 2 builds are completed +# Since we run Unit & Integration tests in parallel, this lets Codecov know that coverage +# needs to be merged across those builds +codecov: + notify: + after_n_builds: 2 + # Settings related to code coverage analysis coverage: status: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..619e31a6f7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,65 @@ +# DSpace Continuous Integration/Build via GitHub Actions +# Concepts borrowed from +# https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-java-with-maven +name: Build + +# Run this Build for all pushes / PRs to current branch +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + env: + # Give Maven 1GB of memory to work with + # Suppress all Maven "downloading" messages in Travis logs (see https://stackoverflow.com/a/35653426) + # This also slightly speeds builds, as there is less logging + MAVEN_OPTS: "-Xmx1024M -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn" + strategy: + # Create a matrix of two separate configurations for Unit vs Integration Tests + # This will ensure those tasks are run in parallel + matrix: + include: + # NOTE: Unit Tests include deprecated REST API v6 (as it has unit tests) + - type: "Unit Tests" + mvnflags: "-DskipUnitTests=false -Pdspace-rest" + # NOTE: ITs skip all code validation checks, as they are already done by Unit Test job. + # - enforcer.skip => Skip maven-enforcer-plugin rules + # - checkstyle.skip => Skip all checkstyle checks by maven-checkstyle-plugin + # - license.skip => Skip all license header checks by license-maven-plugin + # - xml.skip => Skip all XML/XSLT validation by xml-maven-plugin + - type: "Integration Tests" + mvnflags: "-DskipIntegrationTests=false -Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true" + # Do NOT exit immediately if one matrix job fails + # This ensures ITs continue running even if Unit Tests fail, or visa versa + fail-fast: false + # These are the actual CI steps to perform per job + steps: + # https://github.com/actions/checkout + - name: Checkout codebase + uses: actions/checkout@v1 + + # https://github.com/actions/setup-java + - name: Install JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + # https://github.com/actions/cache + - name: Cache Maven dependencies + uses: actions/cache@v2 + with: + # Cache entire ~/.m2/repository + path: ~/.m2/repository + # Cache key is hash of all pom.xml files. Therefore any changes to POMs will invalidate cache + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + + # Run parallel Maven builds based on the above 'strategy.matrix' + - name: Run Maven ${{ matrix.type }} + env: + TEST_FLAGS: ${{ matrix.mvnflags }} + run: mvn install -B -V -P-assembly -Pcoverage-report $TEST_FLAGS + + # https://github.com/codecov/codecov-action + - name: Upload coverage to Codecov.io + uses: codecov/codecov-action@v1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 89cb443597..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,55 +0,0 @@ -# DSpace's Travis CI Configuration -# Builds: https://travis-ci.com/github/DSpace/DSpace -# Travis configuration guide/validation: https://config.travis-ci.com/explore -language: java -# TODO: Upgrade to Bionic -dist: trusty -os: linux - -jdk: - # DS-3384 Oracle JDK has DocLint enabled by default. - # Let's use this to catch any newly introduced DocLint issues. - - oraclejdk11 - -# Define global environment variables (shared across all jobs) -env: - global: - # Suppress all Maven "downloading" messages in Travis logs (see https://stackoverflow.com/a/35653426) - # This also slightly speeds builds in Travis, as there is less logging - - HIDE_MAVEN_DOWNLOADS="-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn" - # Give Maven 1GB of memory to work with - - MAVEN_OPTS="-Xmx1024M $HIDE_MAVEN_DOWNLOADS" - # Maven options which will skip ALL code validation checks. Includes skipping: - # - enforcer.skip => Skip maven-enforcer-plugin rules - # - checkstyle.skip => Skip all checkstyle checks by maven-checkstyle-plugin - # - license.skip => Skip all license header checks by license-maven-plugin - # - xml.skip => Skip all XML/XSLT validation by xml-maven-plugin - # (Useful for builds which don't need to repeat code checks) - - SKIP_CODE_CHECKS="-Denforcer.skip=true -Dcheckstyle.skip=true -Dlicense.skip=true -Dxml.skip=true" - -# Create two jobs to run Unit & Integration tests in parallel. -# These jobs only differ in the TEST_FLAGS defined below, -# and otherwise share all the other configs in this file -jobs: - include: - - name: "Run Unit Tests & Check Code" - # NOTE: unit tests include deprecated REST API v6 (as it has unit tests) - env: TEST_FLAGS="-DskipUnitTests=false -Pdspace-rest" - - name: "Run Integration Tests" - # NOTE: skips code checks, as they are already done by Unit Test job - env: TEST_FLAGS="-DskipIntegrationTests=false $SKIP_CODE_CHECKS" - -# Skip 'install' process to save time. We build/install/test all at once in "script" below. -install: skip - -# Build DSpace and run configured tests (see 'jobs' above) -# Notes on flags used: -# -B => Maven batch/non-interactive mode (recommended for CI) -# -V => Display Maven version info before build -# -P-assembly => Disable build of dspace-installer in [src]/dspace/, as it can be memory intensive -# -Pcoverage-report => Enable aggregate code coverage report (across all modules) via JaCoCo -script: mvn install -B -V -P-assembly -Pcoverage-report $TEST_FLAGS - -# After a successful build and test (see 'script'), send aggregate code coverage reports -# (generated by -Pcoverage-report above) to CodeCov.io -after_success: bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 2e6c0ad54e..b8fee04d3d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # DSpace -[![Build Status](https://travis-ci.com/DSpace/DSpace.png?branch=main)](https://travis-ci.com/DSpace/DSpace) +[![Build Status](https://github.com/DSpace/DSpace/workflows/Build/badge.svg)](https://github.com/DSpace/DSpace/actions?query=workflow%3ABuild) [DSpace Documentation](https://wiki.lyrasis.org/display/DSDOC/) | [DSpace Releases](https://github.com/DSpace/DSpace/releases) | @@ -86,7 +86,7 @@ DSpace uses GitHub to track issues: ### Running Tests By default, in DSpace, Unit Tests and Integration Tests are disabled. However, they are -run automatically by [Travis CI](https://travis-ci.com/DSpace/DSpace/) for all Pull Requests and code commits. +run automatically by [GitHub Actions](https://github.com/DSpace/DSpace/actions?query=workflow%3ABuild) for all Pull Requests and code commits. * How to run both Unit Tests (via `maven-surefire-plugin`) and Integration Tests (via `maven-failsafe-plugin`): ``` diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index b5aa736a8a..f1bbba2f53 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -842,12 +842,30 @@ 1.10.50 - - org.dspace - orcid-jaxb-api - 2.1.0 + org.orcid + orcid-model + 3.0.2 + + + javax.validation + validation-api + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + org.yaml + snakeyaml + + + org.javassist + javassist + + + org.json json diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index c04f64f674..2a75b7cbd2 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -37,6 +37,7 @@ public class OrcidRestConnector { } public InputStream get(String path, String accessToken) { + HttpResponse getResponse = null; InputStream result = null; path = trimSlashes(path); @@ -48,7 +49,7 @@ public class OrcidRestConnector { } try { HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse getResponse = httpClient.execute(httpGet); + getResponse = httpClient.execute(httpGet); //do not close this httpClient result = getResponse.getEntity().getContent(); } catch (Exception e) { @@ -76,6 +77,4 @@ public class OrcidRestConnector { Scanner s = new Scanner(is).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } - - } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV2AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java similarity index 86% rename from dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV2AuthorDataProvider.java rename to dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index e785e2d924..031e5dd8a8 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV2AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -32,30 +32,33 @@ import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.ExternalDataProvider; import org.dspace.external.provider.orcid.xml.XMLtoBio; import org.json.JSONObject; -import org.orcid.jaxb.model.common_v2.OrcidId; -import org.orcid.jaxb.model.record_v2.Person; -import org.orcid.jaxb.model.search_v2.Result; +import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; +import org.orcid.jaxb.model.v3.release.record.Person; +import org.orcid.jaxb.model.v3.release.search.Result; import org.springframework.beans.factory.annotation.Autowired; /** - * This class is the implementation of the ExternalDataProvider interface that will deal with the OrcidV2 External + * This class is the implementation of the ExternalDataProvider interface that will deal with the OrcidV3 External * Data lookup */ -public class OrcidV2AuthorDataProvider implements ExternalDataProvider { +public class OrcidV3AuthorDataProvider implements ExternalDataProvider { - private static final Logger log = LogManager.getLogger(OrcidV2AuthorDataProvider.class); + private static final Logger log = LogManager.getLogger(OrcidV3AuthorDataProvider.class); - private final OrcidRestConnector orcidRestConnector; + private OrcidRestConnector orcidRestConnector; private String OAUTHUrl; - private String clientId; + private String clientId; private String clientSecret; private String accessToken; private String sourceIdentifier; + private String orcidUrl; + private XMLtoBio converter; + public static final String ORCID_ID_SYNTAX = "\\d{4}-\\d{4}-\\d{4}-(\\d{3}X|\\d{4})"; @Override @@ -63,13 +66,18 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { return sourceIdentifier; } + public OrcidV3AuthorDataProvider() { + converter = new XMLtoBio(); + } + /** * Initialize the accessToken that is required for all subsequent calls to ORCID. * * @throws java.io.IOException passed through from HTTPclient. */ public void init() throws IOException { - if (StringUtils.isNotBlank(accessToken) && StringUtils.isNotBlank(clientSecret)) { + if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) + && StringUtils.isNotBlank(OAUTHUrl)) { String authenticationParameters = "?client_id=" + clientId + "&client_secret=" + clientSecret + "&scope=/read-public&grant_type=client_credentials"; @@ -101,14 +109,6 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { } } - /** - * Makes an instance of the Orcidv2 class based on the provided parameters. - * This constructor is called through the spring bean initialization - */ - private OrcidV2AuthorDataProvider(String url) { - this.orcidRestConnector = new OrcidRestConnector(url); - } - @Override public Optional getExternalDataObject(String id) { Person person = getBio(id); @@ -121,12 +121,12 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { String lastName = ""; String firstName = ""; if (person.getName().getFamilyName() != null) { - lastName = person.getName().getFamilyName().getValue(); + lastName = person.getName().getFamilyName().getContent(); externalDataObject.addMetadata(new MetadataValueDTO("person", "familyName", null, null, lastName)); } if (person.getName().getGivenNames() != null) { - firstName = person.getName().getGivenNames().getValue(); + firstName = person.getName().getGivenNames().getContent(); externalDataObject.addMetadata(new MetadataValueDTO("person", "givenName", null, null, firstName)); @@ -150,7 +150,7 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { } /** - * Retrieve a Person object based on a given orcid identifier + * Retrieve a Person object based on a given orcid identifier. * @param id orcid identifier * @return Person */ @@ -160,7 +160,6 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { return null; } InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - XMLtoBio converter = new XMLtoBio(); Person person = converter.convertSinglePerson(bioDocument); try { bioDocument.close(); @@ -190,14 +189,13 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { + "&rows=" + limit; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - XMLtoBio converter = new XMLtoBio(); List results = converter.convert(bioDocument); List bios = new LinkedList<>(); for (Result result : results) { - OrcidId orcidIdentifier = result.getOrcidIdentifier(); + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); if (orcidIdentifier != null) { log.debug("Found OrcidId=" + orcidIdentifier.toString()); - String orcid = orcidIdentifier.getUriPath(); + String orcid = orcidIdentifier.getPath(); Person bio = getBio(orcid); if (bio != null) { bios.add(bio); @@ -228,14 +226,13 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { + "&rows=" + 0; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - XMLtoBio converter = new XMLtoBio(); return converter.getNumberOfResultsFromXml(bioDocument); } /** * Generic setter for the sourceIdentifier - * @param sourceIdentifier The sourceIdentifier to be set on this OrcidV2AuthorDataProvider + * @param sourceIdentifier The sourceIdentifier to be set on this OrcidV3AuthorDataProvider */ @Autowired(required = true) public void setSourceIdentifier(String sourceIdentifier) { @@ -244,7 +241,7 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { /** * Generic getter for the orcidUrl - * @return the orcidUrl value of this OrcidV2AuthorDataProvider + * @return the orcidUrl value of this OrcidV3AuthorDataProvider */ public String getOrcidUrl() { return orcidUrl; @@ -252,7 +249,7 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { /** * Generic setter for the orcidUrl - * @param orcidUrl The orcidUrl to be set on this OrcidV2AuthorDataProvider + * @param orcidUrl The orcidUrl to be set on this OrcidV3AuthorDataProvider */ @Autowired(required = true) public void setOrcidUrl(String orcidUrl) { @@ -261,7 +258,7 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { /** * Generic setter for the OAUTHUrl - * @param OAUTHUrl The OAUTHUrl to be set on this OrcidV2AuthorDataProvider + * @param OAUTHUrl The OAUTHUrl to be set on this OrcidV3AuthorDataProvider */ public void setOAUTHUrl(String OAUTHUrl) { this.OAUTHUrl = OAUTHUrl; @@ -269,7 +266,7 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { /** * Generic setter for the clientId - * @param clientId The clientId to be set on this OrcidV2AuthorDataProvider + * @param clientId The clientId to be set on this OrcidV3AuthorDataProvider */ public void setClientId(String clientId) { this.clientId = clientId; @@ -277,9 +274,18 @@ public class OrcidV2AuthorDataProvider implements ExternalDataProvider { /** * Generic setter for the clientSecret - * @param clientSecret The clientSecret to be set on this OrcidV2AuthorDataProvider + * @param clientSecret The clientSecret to be set on this OrcidV3AuthorDataProvider */ public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } + + public OrcidRestConnector getOrcidRestConnector() { + return orcidRestConnector; + } + + public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { + this.orcidRestConnector = orcidRestConnector; + } + } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java index 74a348322b..25b3cf787f 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java @@ -13,11 +13,12 @@ import java.util.ArrayList; import java.util.List; import org.apache.logging.log4j.Logger; -import org.orcid.jaxb.model.record_v2.Person; -import org.orcid.jaxb.model.search_v2.Result; -import org.orcid.jaxb.model.search_v2.Search; +import org.orcid.jaxb.model.v3.release.record.Person; +import org.orcid.jaxb.model.v3.release.search.Result; +import org.orcid.jaxb.model.v3.release.search.Search; import org.xml.sax.SAXException; + /** * @author Antoine Snyers (antoine at atmire.com) * @author Kevin Van de Velde (kevin at atmire dot com) @@ -36,7 +37,7 @@ public class XMLtoBio extends Converter> { List bios = new ArrayList<>(); try { Search search = (Search) unmarshall(xml, Search.class); - bios = search.getResult(); + bios = search.getResults(); } catch (SAXException | URISyntaxException e) { log.error(e); } @@ -52,6 +53,7 @@ public class XMLtoBio extends Converter> { } return 0; } + public Person convertSinglePerson(InputStream xml) { Person person = null; try { diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml index a341d4db5b..06f490eecc 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml @@ -10,5 +10,17 @@ + + + + + + + + + + + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 799f6b7892..25048760c2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -24,10 +24,11 @@ public class ExternalSourcesRestControllerIT extends AbstractControllerIntegrati public void findAllExternalSources() throws Exception { getClient().perform(get("/api/integration/externalsources")) .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded.externalsources", Matchers.hasItem( - ExternalSourceMatcher.matchExternalSource("mock", "mock", false) + .andExpect(jsonPath("$._embedded.externalsources", Matchers.hasItems( + ExternalSourceMatcher.matchExternalSource("mock", "mock", false), + ExternalSourceMatcher.matchExternalSource("orcid", "orcid", false) ))) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); + .andExpect(jsonPath("$.page.totalElements", Matchers.is(2))); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java new file mode 100644 index 0000000000..d1acd482b8 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java @@ -0,0 +1,281 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.InputStream; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.external.OrcidRestConnector; +import org.dspace.external.provider.impl.OrcidV3AuthorDataProvider; +import org.dspace.services.ConfigurationService; +import org.hamcrest.Matchers; +import org.junit.Assume; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * This test suite includes static test with mock data and end to end test to + * verify the integration with ORCID as an External Source. The end to end test + * run only if the orcid.clientid property is configured but of course also + * orcid.clientsecret is needed to successful run the tests. This can be enabled + * setting the orcid credentials via env variables, see the comments in the + * override section of the config-definition.xml + * + * @author Mykhaylo Boychuk (4Science.it) + * + */ +public class OrcidExternalSourcesIT extends AbstractControllerIntegrationTest { + + @Autowired + ConfigurationService configurationService; + + @Autowired + private OrcidV3AuthorDataProvider orcidV3AuthorDataProvider; + + public void onlyRunIfConfigExists() { + if (StringUtils.isBlank(configurationService.getProperty("orcid.clientid"))) { + Assume.assumeNoException(new IllegalStateException("Missing ORCID credentials")); + } + } + + @Test + public void findOneExternalSourcesExistingSources() throws Exception { + getClient().perform(get("/api/integration/externalsources/orcid")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is("orcid")), + hasJsonPath("$.name", is("orcid")), + hasJsonPath("$.hierarchical", is(false)), + hasJsonPath("$.type", is("externalsource")) + ))); + } + + @Test + public void findOneExternalSourcesExistingSourcesWithentryValueTest() throws Exception { + // this test will query the real ORCID API if configured in the CI otherwise will be skipped + onlyRunIfConfigExists(); + String entry = "0000-0002-9029-1854"; + getClient().perform(get("/api/integration/externalsources/orcid/entryValues/" + entry)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(entry)), + hasJsonPath("$.display", is("Bollini, Andrea")), + hasJsonPath("$.value", is("Bollini, Andrea")), + hasJsonPath("$.externalSource", is("orcid")), + hasJsonPath("$.type", is("externalSourceEntry")) + ))) + .andExpect(jsonPath("$.metadata['dc.identifier.uri'][0].value",is("https://orcid.org/" + entry))) + .andExpect(jsonPath("$.metadata['person.familyName'][0].value",is("Bollini"))) + .andExpect(jsonPath("$.metadata['person.givenName'][0].value",is("Andrea"))) + .andExpect(jsonPath("$.metadata['person.identifier.orcid'][0].value",is(entry))); + } + + @Test + public void findOneExternalSourceEntriesApplicableQueryTest() throws Exception { + // this test will query the real ORCID API if configured in the CI otherwise will be skipped + onlyRunIfConfigExists(); + String q = "orcid:0000-0002-9029-1854"; + getClient().perform(get("/api/integration/externalsources/orcid/entries") + .param("query", q)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0]", Matchers.allOf( + hasJsonPath("$.id", is("0000-0002-9029-1854")), + hasJsonPath("$.display", is("Bollini, Andrea")), + hasJsonPath("$.value", is("Bollini, Andrea")), + hasJsonPath("$.externalSource", is("orcid")), + hasJsonPath("$.type", is("externalSourceEntry")) + ))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['dc.identifier.uri'][0].value", + is("https://orcid.org/0000-0002-9029-1854"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.familyName'][0].value", + is("Bollini"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.givenName'][0].value", + is("Andrea"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.identifier.orcid'][0].value", + is("0000-0002-9029-1854"))); + } + + @Test + public void findOneExternalSourceEntriesApplicableQueryFamilyNameAndGivenNamesTest() throws Exception { + // this test will query the real ORCID API if configured in the CI otherwise will be skipped + onlyRunIfConfigExists(); + String q = "family-name:bollini AND given-names:andrea"; + getClient().perform(get("/api/integration/externalsources/orcid/entries") + .param("query", q)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.externalSourceEntries", Matchers.hasItem( + Matchers.allOf( + hasJsonPath("$.id", is("0000-0002-9029-1854")), + hasJsonPath("$.display", is("Bollini, Andrea")), + hasJsonPath("$.value", is("Bollini, Andrea")), + hasJsonPath("$.externalSource", is("orcid")), + hasJsonPath("$.type", is("externalSourceEntry"))) + ))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['dc.identifier.uri'][0].value", + is("https://orcid.org/0000-0002-9029-1854"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.familyName'][0].value", + is("Bollini"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.givenName'][0].value", + is("Andrea"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.identifier.orcid'][0].value", + is("0000-0002-9029-1854"))); + } + + @Test + /** + * This test uses mock data in the orcid-person-record.xml file to simulate the + * response from ORCID and verify that it is properly consumed and exposed by + * the REST API + * + * @throws Exception + */ + public void findOneExternalSourcesMockitoTest() throws Exception { + OrcidRestConnector orcidConnector = Mockito.mock(OrcidRestConnector.class); + OrcidRestConnector realConnector = orcidV3AuthorDataProvider.getOrcidRestConnector(); + orcidV3AuthorDataProvider.setOrcidRestConnector(orcidConnector); + when(orcidConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) + .thenAnswer(new Answer() { + public InputStream answer(InvocationOnMock invocation) { + return getClass().getResourceAsStream("orcid-person-record.xml"); + } + }); + + String entry = "0000-0002-9029-1854"; + getClient().perform(get("/api/integration/externalsources/orcid/entryValues/" + entry)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(entry)), + hasJsonPath("$.display", is("Bollini, Andrea")), + hasJsonPath("$.value", is("Bollini, Andrea")), + hasJsonPath("$.externalSource", is("orcid")), + hasJsonPath("$.type", is("externalSourceEntry")) + ))); + + orcidV3AuthorDataProvider.setOrcidRestConnector(realConnector); + } + + @Test + /** + * This test uses mock data in the orcid-search.xml and orcid-person-record.xml + * file to simulate the response from ORCID and verify that it is properly + * consumed and exposed by the REST API. The orcid-search.xml file indeed + * contains the ORCID matching the user query, for each of them our integration + * need to grab details making a second call to the ORCID profile (this is due + * to the ORCID API structure and cannot be avoid) + * + * @throws Exception + */ + public void findOneExternalSourceEntriesApplicableQueryMockitoTest() throws Exception { + OrcidRestConnector orcidConnector = Mockito.mock(OrcidRestConnector.class); + OrcidRestConnector realConnector = orcidV3AuthorDataProvider.getOrcidRestConnector(); + orcidV3AuthorDataProvider.setOrcidRestConnector(orcidConnector); + try { + when(orcidConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) + .thenAnswer(new Answer() { + public InputStream answer(InvocationOnMock invocation) { + return getClass().getResourceAsStream("orcid-search.xml"); + } + }); + when(orcidConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) + .thenAnswer(new Answer() { + public InputStream answer(InvocationOnMock invocation) { + return getClass().getResourceAsStream("orcid-person-record.xml"); + } + }); + String q = "orcid:0000-0002-9029-1854"; + getClient().perform(get("/api/integration/externalsources/orcid/entries") + .param("query", q)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0]", Matchers.allOf( + hasJsonPath("$.id", is("0000-0002-9029-1854")), + hasJsonPath("$.display", is("Bollini, Andrea")), + hasJsonPath("$.value", is("Bollini, Andrea")), + hasJsonPath("$.externalSource", is("orcid")), + hasJsonPath("$.type", is("externalSourceEntry")) + ))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['dc.identifier.uri'][0].value", + is("https://orcid.org/0000-0002-9029-1854"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.familyName'][0].value", + is("Bollini"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.givenName'][0].value", + is("Andrea"))) + .andExpect(jsonPath( + "$._embedded.externalSourceEntries[0].metadata['person.identifier.orcid'][0].value", + is("0000-0002-9029-1854"))); + } finally { + orcidV3AuthorDataProvider.setOrcidRestConnector(realConnector); + } + } + + @Test + /** + * This test uses mock data in the orcid-search.xml and orcid-person-record.xml + * file to simulate the response from ORCID and verify that it is properly + * consumed and exposed by the REST API. The orcid-search.xml file indeed + * contains the ORCID matching the user query, for each of them our integration + * need to grab details making a second call to the ORCID profile (this is due + * to the ORCID API structure and cannot be avoid) + * + * @throws Exception + */ + public void findOneExternalSourceEntriesApplicableQueryFamilyNameAndGivenNamesMockitoTest() throws Exception { + OrcidRestConnector orcidConnector = Mockito.mock(OrcidRestConnector.class); + OrcidRestConnector realConnector = orcidV3AuthorDataProvider.getOrcidRestConnector(); + orcidV3AuthorDataProvider.setOrcidRestConnector(orcidConnector); + try { + when(orcidConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) + .thenAnswer(new Answer() { + public InputStream answer(InvocationOnMock invocation) { + return getClass().getResourceAsStream("orcid-search.xml"); + } + }); + when(orcidConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) + .thenAnswer(new Answer() { + public InputStream answer(InvocationOnMock invocation) { + return getClass().getResourceAsStream("orcid-person-record.xml"); + } + }); + String q = "family-name:bollini AND given-names:andrea"; + getClient().perform(get("/api/integration/externalsources/orcid/entries") + .param("query", q)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.externalSourceEntries", Matchers.hasItem( + Matchers.allOf( + hasJsonPath("$.id", is("0000-0002-9029-1854")), + hasJsonPath("$.display", is("Bollini, Andrea")), + hasJsonPath("$.value", is("Bollini, Andrea")), + hasJsonPath("$.externalSource", is("orcid")), + hasJsonPath("$.type", is("externalSourceEntry"))) + ))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['dc.identifier.uri'][0].value", + is("https://orcid.org/0000-0002-9029-1854"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.familyName'][0].value", + is("Bollini"))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].metadata['person.givenName'][0].value", + is("Andrea"))) + .andExpect(jsonPath( + "$._embedded.externalSourceEntries[0].metadata['person.identifier.orcid'][0].value", + is("0000-0002-9029-1854"))); + } finally { + orcidV3AuthorDataProvider.setOrcidRestConnector(realConnector); + } + } + +} diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-person-record.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-person-record.xml new file mode 100644 index 0000000000..ed51b8e50d --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-person-record.xml @@ -0,0 +1,234 @@ + + + 2018-02-05T23:27:36.636Z + + 2016-04-15T23:17:03.663Z + 2016-04-15T23:17:03.663Z + Andrea + Bollini + + + + 2017-07-18T15:10:48.940Z + + 2017-01-16T08:12:12.946Z + 2017-07-18T15:10:48.940Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + Linkedin + https://it.linkedin.com/in/andreabollini + + + 2014-11-06T10:37:30.383Z + 2017-07-18T15:10:48.940Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + 4Science + http://www.4science.it/en/ + + + 2014-11-06T10:37:30.412Z + 2017-07-18T15:10:48.940Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + DSpace + http://www.dspace.org + + + 2014-11-06T10:37:30.398Z + 2017-07-18T15:10:48.940Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + DSpace-CRIS + https://wiki.duraspace.org/display/DSPACECRIS + + + + 2016-09-12T11:22:47.354Z + + 2016-09-12T10:45:26.123Z + 2016-09-12T11:22:47.354Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + andrea.bollini@4science.it + + + + 2016-06-06T15:29:36.952Z + + 2016-01-24T18:24:26.704Z + 2016-06-06T15:29:36.952Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + IT + + + + 2016-03-01T11:03:22.508Z + + 2013-05-30T10:55:45.614Z + 2016-03-01T11:03:22.508Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + Software + + + 2013-05-30T10:55:45.614Z + 2016-03-01T11:03:22.508Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + Open Source + + + 2013-05-30T10:55:45.614Z + 2016-03-01T11:03:22.508Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + JAVA + + + 2016-03-01T11:03:22.491Z + 2016-03-01T11:03:22.491Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + CERIF + + + 2016-03-01T11:03:22.502Z + 2016-03-01T11:03:22.502Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + CRIS + + + 2016-03-01T11:03:22.503Z + 2016-03-01T11:03:22.503Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + RIMS + + + 2016-03-01T11:03:22.504Z + 2016-03-01T11:03:22.504Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + Open Standards + + + 2016-03-01T11:03:22.505Z + 2016-03-01T11:03:22.505Z + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + OAI-PMH + + + + 2018-02-05T23:27:36.636Z + + 2013-05-30T10:55:45.614Z + 2018-02-05T23:27:36.636Z + + + https://orcid.org/client/0000-0002-5982-8983 + 0000-0002-5982-8983 + orcid.org + + Scopus - Elsevier + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + Andrea Bollini + + Scopus Author ID + 55484808800 + http://www.scopus.com/inward/authorDetails.url?authorID=55484808800&partnerID=MN8TOARS + self + + + \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-search.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-search.xml new file mode 100644 index 0000000000..3f7fe9a284 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-search.xml @@ -0,0 +1,10 @@ + + + + + https://orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + orcid.org + + + \ No newline at end of file diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 96b8ddf536..030d09dbd7 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1473,10 +1473,20 @@ sherpa.romeo.apikey = # org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority # URL of ORCID API -# Defaults to using the Public API (pub.orcid.org) -orcid.api.url = https://pub.orcid.org/v2.1 +# Defaults to using the Public API V3 (pub.orcid.org) +orcid.api.url = https://pub.orcid.org/v3.0 orcid.url = https://orcid.org/ +# ORCID Credentials +# Your public or member API Credentials, see +# https://orcid.org/content/register-client-application-0 +orcid.clientid = +orcid.clientsecret = + +#ORCID JWT Endpoint +orcid.oauth.url = https://orcid.org/oauth/token + + ## The DCInputAuthority plugin is automatically configured with every ## value-pairs element in input-forms.xml, namely: ## common_identifiers, common_types, common_iso_languages diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index b56870b24b..41a5fd517f 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -20,10 +20,17 @@ - - - + + + + + + + + + +