Skip to content

Commit

Permalink
More robust ORCID access token initialization
Browse files Browse the repository at this point in the history
Ensure that http client / IO exceptions don't cause a
total DSpace startup failure because of unhandled
exceptions in Spring service init methods.
Centralise access token retrieval method in factory utils.
  • Loading branch information
kshepherd committed Oct 7, 2024
1 parent e8a2e73 commit 04096c1
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,21 @@
*/
package org.dspace.authority.orcid;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authority.AuthorityValue;
import org.dspace.authority.SolrAuthorityInterface;
import org.dspace.external.OrcidRestConnector;
import org.dspace.external.provider.orcid.xml.XMLtoBio;
import org.json.JSONObject;
import org.dspace.orcid.model.factory.OrcidFactoryUtils;
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;
Expand All @@ -50,6 +44,11 @@ public class Orcidv3SolrAuthorityImpl implements SolrAuthorityInterface {

private String accessToken;

/**
* Maximum retries to allow for the access token retrieval
*/
private int maxClientRetries = 3;

public void setOAUTHUrl(String oAUTHUrl) {
OAUTHUrl = oAUTHUrl;
}
Expand All @@ -66,42 +65,20 @@ public void setClientSecret(String clientSecret) {
* Initialize the accessToken that is required for all subsequent calls to ORCID
*/
public void init() {
if (StringUtils.isBlank(accessToken)
&& StringUtils.isNotBlank(clientSecret)
&& StringUtils.isNotBlank(clientId)
&& StringUtils.isNotBlank(OAUTHUrl)) {
String authenticationParameters = "?client_id=" + clientId +
"&client_secret=" + clientSecret +
"&scope=/read-public&grant_type=client_credentials";
try {
HttpPost httpPost = new HttpPost(OAUTHUrl + authenticationParameters);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");

HttpClient httpClient = HttpClientBuilder.create().build();
HttpResponse getResponse = httpClient.execute(httpPost);

JSONObject responseObject = null;
try (InputStream is = getResponse.getEntity().getContent();
BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
String inputStr;
while ((inputStr = streamReader.readLine()) != null && responseObject == null) {
if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) {
try {
responseObject = new JSONObject(inputStr);
} catch (Exception e) {
//Not as valid as I'd hoped, move along
responseObject = null;
}
}
}
}
if (responseObject != null && responseObject.has("access_token")) {
accessToken = (String) responseObject.get("access_token");
}
} catch (Exception e) {
throw new RuntimeException("Error during initialization of the Orcid connector", e);
}
// Initialize access token at spring instantiation. If it fails, the access token will be null rather
// than causing a fatal Spring startup error
initializeAccessToken();
}

public void initializeAccessToken() {
// If we have reaches max retries or the access token is already set, return immediately
if (maxClientRetries <= 0 || org.apache.commons.lang3.StringUtils.isNotBlank(accessToken)) {
return;
}
try {
accessToken = OrcidFactoryUtils.retrieveAccessToken(clientId, clientSecret, OAUTHUrl).orElse(null);
} catch (IOException e) {
log.error("Error retrieving ORCID access token, {} retries left", --maxClientRetries);
}
}

Expand All @@ -116,7 +93,7 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) {
*/
@Override
public List<AuthorityValue> queryAuthorities(String text, int max) {
init();
initializeAccessToken();
List<Person> bios = queryBio(text, max);
List<AuthorityValue> result = new ArrayList<>();
for (Person person : bios) {
Expand All @@ -135,7 +112,7 @@ public List<AuthorityValue> queryAuthorities(String text, int max) {
*/
@Override
public AuthorityValue queryAuthorityID(String id) {
init();
initializeAccessToken();
Person person = getBio(id);
AuthorityValue valueFromPerson = Orcidv3AuthorityValue.create(person);
return valueFromPerson;
Expand All @@ -151,7 +128,7 @@ public Person getBio(String id) {
if (!isValid(id)) {
return null;
}
init();
initializeAccessToken();
InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken);
XMLtoBio converter = new XMLtoBio();
Person person = converter.convertSinglePerson(bioDocument);
Expand All @@ -167,7 +144,7 @@ public Person getBio(String id) {
* @return List<Person>
*/
public List<Person> queryBio(String text, int start, int rows) {
init();
initializeAccessToken();
if (rows > 100) {
throw new IllegalArgumentException("The maximum number of results to retrieve cannot exceed 100.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
*/
package org.dspace.external.provider.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
Expand All @@ -21,18 +19,14 @@
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.external.OrcidRestConnector;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.AbstractExternalDataProvider;
import org.dspace.external.provider.orcid.xml.XMLtoBio;
import org.json.JSONObject;
import org.dspace.orcid.model.factory.OrcidFactoryUtils;
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;
Expand Down Expand Up @@ -60,6 +54,11 @@ public class OrcidV3AuthorDataProvider extends AbstractExternalDataProvider {

private XMLtoBio converter;

/**
* Maximum retries to allow for the access token retrieval
*/
private int maxClientRetries = 3;

public static final String ORCID_ID_SYNTAX = "\\d{4}-\\d{4}-\\d{4}-(\\d{3}X|\\d{4})";
private static final int MAX_INDEX = 10000;

Expand All @@ -78,47 +77,37 @@ public OrcidV3AuthorDataProvider() {
* @throws java.io.IOException passed through from HTTPclient.
*/
public void init() throws IOException {
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";
HttpPost httpPost = new HttpPost(OAUTHUrl + authenticationParameters);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");

HttpClient httpClient = HttpClientBuilder.create().build();
HttpResponse getResponse = httpClient.execute(httpPost);

JSONObject responseObject = null;
try (InputStream is = getResponse.getEntity().getContent();
BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
String inputStr;
while ((inputStr = streamReader.readLine()) != null && responseObject == null) {
if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) {
try {
responseObject = new JSONObject(inputStr);
} catch (Exception e) {
//Not as valid as I'd hoped, move along
responseObject = null;
}
}
}
}
if (responseObject != null && responseObject.has("access_token")) {
accessToken = (String) responseObject.get("access_token");
}
// Initialize access token at spring instantiation. If it fails, the access token will be null rather
// than causing a fatal Spring startup error
initializeAccessToken();
}

/**
* Initialize access token, logging an error and decrementing remaining retries if an IOException is thrown.
* If the optional access token result is empty, set to null instead.
*/
public void initializeAccessToken() {
// If we have reaches max retries or the access token is already set, return immediately
if (maxClientRetries <= 0 || StringUtils.isNotBlank(accessToken)) {
return;
}
try {
accessToken = OrcidFactoryUtils.retrieveAccessToken(clientId, clientSecret, OAUTHUrl).orElse(null);
} catch (IOException e) {
log.error("Error retrieving ORCID access token, {} retries left", --maxClientRetries);
}
}

@Override
public Optional<ExternalDataObject> getExternalDataObject(String id) {
initializeAccessToken();
Person person = getBio(id);
ExternalDataObject externalDataObject = convertToExternalDataObject(person);
return Optional.of(externalDataObject);
}

protected ExternalDataObject convertToExternalDataObject(Person person) {
initializeAccessToken();
ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier);
if (person.getName() != null) {
String lastName = "";
Expand Down Expand Up @@ -167,6 +156,7 @@ public Person getBio(String id) {
if (!isValid(id)) {
return null;
}
initializeAccessToken();
InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken);
Person person = converter.convertSinglePerson(bioDocument);
try {
Expand All @@ -188,6 +178,7 @@ private boolean isValid(String text) {

@Override
public List<ExternalDataObject> searchExternalDataObjects(String query, int start, int limit) {
initializeAccessToken();
if (limit > 100) {
throw new IllegalArgumentException("The maximum number of results to retrieve cannot exceed 100.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@
*/
package org.dspace.orcid.model.factory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;

/**
* Utility class for Orcid factory classes. This is used to parse the
Expand All @@ -22,6 +35,8 @@
*/
public final class OrcidFactoryUtils {

private static final Logger log = LogManager.getLogger();

private OrcidFactoryUtils() {

}
Expand Down Expand Up @@ -65,4 +80,48 @@ private static String[] parseConfiguration(String configuration) {
return configurations;
}

/**
* Retrieve access token from ORCID, given a client ID, client secret and OAuth URL
*
* @param clientId ORCID client ID
* @param clientSecret ORCID client secret
* @param oauthUrl ORCID oauth redirect URL
* @return response object as Optional string
* @throws IOException if any errors are encountered making the connection or reading a response
*/
public static Optional<String> retrieveAccessToken(String clientId, String clientSecret, String oauthUrl)
throws IOException {
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";
HttpPost httpPost = new HttpPost(oauthUrl + authenticationParameters);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");

HttpResponse response;
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
response = httpClient.execute(httpPost);
}
JSONObject responseObject = null;
if (response != null && response.getStatusLine().getStatusCode() == 200) {
try (InputStream is = response.getEntity().getContent();
BufferedReader streamReader = new BufferedReader(new InputStreamReader(is,
StandardCharsets.UTF_8))) {
String inputStr;
while ((inputStr = streamReader.readLine()) != null && responseObject == null) {
if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) {
responseObject = new JSONObject(inputStr);
}
}
}
}
if (responseObject != null && responseObject.has("access_token")) {
return Optional.of((String) responseObject.get("access_token"));
}
}
// Return empty by default
return Optional.empty();
}
}

0 comments on commit 04096c1

Please sign in to comment.