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
-[](https://travis-ci.com/DSpace/DSpace)
+[](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 @@
-
-
-
+
+
+
+
+
+
+
+
+
+