Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package life.qbic.datamanager;

import java.util.Objects;
import life.qbic.broadcasting.Exchange;
import life.qbic.broadcasting.MessageBusSubmission;
import life.qbic.domain.concepts.SimpleEventStore;
import life.qbic.domain.concepts.TemporaryEventRepository;
import life.qbic.identity.api.UserInformationService;
import life.qbic.identity.api.UserPasswordService;
import life.qbic.identity.application.communication.EmailService;
import life.qbic.identity.application.communication.broadcasting.EventHub;
import life.qbic.identity.application.notification.NotificationService;
Expand All @@ -28,13 +28,13 @@
import life.qbic.projectmanagement.application.AppContextProvider;
import life.qbic.projectmanagement.application.OrganisationRepository;
import life.qbic.projectmanagement.application.ProjectInformationService;
import life.qbic.projectmanagement.application.concurrent.ElasticScheduler;
import life.qbic.projectmanagement.application.concurrent.VirtualThreadScheduler;
import life.qbic.projectmanagement.application.api.SampleCodeService;
import life.qbic.projectmanagement.application.authorization.acl.ProjectAccessService;
import life.qbic.projectmanagement.application.authorization.authorities.AuthorityService;
import life.qbic.projectmanagement.application.batch.BatchRegistrationService;
import life.qbic.projectmanagement.application.communication.broadcasting.MessageRouter;
import life.qbic.projectmanagement.application.concurrent.ElasticScheduler;
import life.qbic.projectmanagement.application.concurrent.VirtualThreadScheduler;
import life.qbic.projectmanagement.application.experiment.ExperimentInformationService;
import life.qbic.projectmanagement.application.measurement.MeasurementLookupService;
import life.qbic.projectmanagement.application.policy.BatchRegisteredPolicy;
Expand Down Expand Up @@ -115,8 +115,10 @@ public IdentityService userRegistrationService(
}

@Bean
public OrganisationRepository organisationRepository() {
return new CachedOrganisationRepository();
public OrganisationRepository organisationRepository(
@Value("${qbic.external-service.organisation-search.ror.client-id}") String clientId) {
Objects.requireNonNull(clientId);
return new CachedOrganisationRepository(clientId);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ qbic.external-service.person-search.orcid.extended-search-uri=${qbic.external-se
qbic.external-service.person-search.orcid.scope=/read-public
qbic.external-service.person-search.orcid.grant-type=client_credentials
qbic.external-service.person-search.orcid.issuer=${ORCID_SEARCH_ISSUER_URL:https://orcid.org}
qbic.external-service.organisation-search.ror.client-id=${ROR_CLIENT_ID}

###############################################################################
################### ActiveMQ Artemis ##########################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
Expand All @@ -40,23 +41,44 @@ public class CachedOrganisationRepository implements OrganisationRepository {

private static final Logger log = logger(CachedOrganisationRepository.class);
private static final int DEFAULT_CACHE_SIZE = 50;
private static final String ROR_API_URL = "https://api.ror.org/v1/organizations/%s";
private static final String ROR_API_URL = "https://api.ror.org/v2/organizations/%s";
private static final String ROR_ID_PATTERN = "0[a-z|0-9]{6}[0-9]{2}$";
private final Map<String, String> iriToOrganisation = new HashMap<>();
private final int configuredCacheSize;

private final String apiClientId;
private boolean cacheUsedForLastRequest = false;



public CachedOrganisationRepository(int cacheSize) {
public CachedOrganisationRepository(int cacheSize, String apiClientId) {
this.configuredCacheSize = cacheSize;
this.apiClientId = Objects.requireNonNull(apiClientId);
}

public CachedOrganisationRepository(String apiClientId) {
this.apiClientId = Objects.requireNonNull(apiClientId);
this.configuredCacheSize = DEFAULT_CACHE_SIZE;
}

public CachedOrganisationRepository() {
/**
* Creates a Repository without an API Key. Not advised for use in production.
*/
protected CachedOrganisationRepository() {
log.warn(
"Creating organisation repository without API Key. This is not advised to be used in production.");
apiClientId = null;
this.configuredCacheSize = DEFAULT_CACHE_SIZE;
}

/**
* Creates a Repository without an API Key. Not advised for use in production.
*/
protected CachedOrganisationRepository(int cacheSize) {
log.warn(
"Creating organisation repository without API Key. This is not advised to be used in production.");
apiClientId = null;
this.configuredCacheSize = cacheSize;
}

private static Optional<String> extractRorId(String text) {
var pattern = Pattern.compile(ROR_ID_PATTERN);
var matcher = pattern.matcher(text);
Expand All @@ -81,27 +103,45 @@ private Optional<Organisation> lookupROR(String iri) {
return extractRorId(iri).map(this::findOrganisationInROR).or(Optional::empty);
}

private Optional<String> apiKey() {
return Optional.ofNullable(apiClientId);
}

private Organisation findOrganisationInROR(String rorId) {
try {
HttpClient client = HttpClient.newBuilder().version(Version.HTTP_2)
.followRedirects(Redirect.NORMAL).connectTimeout(
Duration.ofSeconds(10)).build();
HttpRequest rorQuery = HttpRequest.newBuilder().uri(URI.create(ROR_API_URL.formatted(rorId)))
.header("Content-Type", "application/json").GET().build();

var queryBuilder = HttpRequest.newBuilder().uri(URI.create(ROR_API_URL.formatted(rorId)))
.header("Content-Type", "application/json").GET();
apiKey().ifPresentOrElse(apiKey ->
queryBuilder.header("Client-Id", apiClientId),
() -> log.warn("No ROR API Key provided."));

var rorQuery = queryBuilder.build();
var result = client.send(rorQuery, BodyHandlers.ofString());
//If a valid RoRId was provided but the ID does not exist we fail
if (result.statusCode() != 200) {
if (result.statusCode() == 404) {
log.warn(
"Provided Organisation ROR id: %s was not found via API call to %s".formatted(rorId,
ROR_API_URL));
return null;
}
RORentry rorEntry = new ObjectMapper().configure(
//If a valid RoRId was provided but the ID does not exist we fail
else if (result.statusCode() != 200) {
log.warn(
("Unexpected error retrieving an organization with ROR id %s. API call to %s").formatted(
rorId,
ROR_API_URL));
return null;
}
RorEntryV2 rorEntry = new ObjectMapper().configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.readValue(result.body(), RORentry.class);
.readValue(result.body(), RorEntryV2.class);
updateCache(rorEntry);
cacheUsedForLastRequest = false;
return new Organisation(rorEntry.getId(), rorEntry.getName());
return new Organisation(rorEntry.getId(), rorEntry.getDisplayedName());
} catch (IOException | InterruptedException e) {
log.error("Finding ROR entry failed for organisation: %s".formatted(rorId), e);
/* Clean up whatever needs to be handled before interrupting */
Expand All @@ -110,12 +150,12 @@ private Organisation findOrganisationInROR(String rorId) {
}
}

private void updateCache(RORentry rorEntry) {
private void updateCache(RorEntryV2 rorEntry) {
if (iriToOrganisation.size() == configuredCacheSize) {
String firstKey = iriToOrganisation.keySet().stream().toList().get(0);
iriToOrganisation.remove(firstKey);
}
iriToOrganisation.put(rorEntry.getId(), rorEntry.getName());
iriToOrganisation.put(rorEntry.getId(), rorEntry.getDisplayedName());
}

public int cacheEntries() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package life.qbic.projectmanagement.infrastructure.organisations;

public interface RorEntry {

String getId();

String getDisplayedName();

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* @since 1.0.0
*/
public class RORentry {
public class RorEntryV1 implements RorEntry {

@JsonProperty("id")
String id;
Expand All @@ -32,6 +32,11 @@ public String getId() {
return id;
}

@Override
public String getDisplayedName() {
return getName();
}

public void setId(String id) {
if (id == null) {
id = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package life.qbic.projectmanagement.infrastructure.organisations;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

public class RorEntryV2 implements RorEntry {

@JsonProperty("id")
String id;

@JsonProperty("names")
List<OrganisationName> names;

public static class OrganisationName {

@JsonProperty("lang")
String language;

@JsonProperty("types")
List<String> types;

@JsonProperty("value")
String value;

public String getLanguage() {
return language;
}

public List<String> getTypes() {
return types;
}

public String getValue() {
return value;
}
}

public String getId() {
return id;
}

@Override
public String getDisplayedName() {
return names.stream().filter(name -> name.getTypes().contains("ror_display")).findFirst()
.map(OrganisationName::getValue)
.orElseThrow();
}
}
Loading