From 0c1d0a149bfd3fd116860e53b73be208f85b7580 Mon Sep 17 00:00:00 2001 From: Nicolas Rod <Nicolas.Rod@unige.ch> Date: Thu, 23 Jan 2025 09:32:01 +0100 Subject: [PATCH 1/4] feat(ORCID): solidify OrcidSynchronization --- .../unige/solidify/auth/model/OrcidInfo.java | 10 +- .../solidify/model/OrcidSynchronization.java | 120 ++++++++++++ .../unige/solidify/model/PersonWithOrcid.java | 9 +- .../business/OrcidSynchronizationService.java | 64 +++++++ .../solidify/controller/OrcidController.java | 18 +- .../OrcidSynchronizationRepository.java | 43 +++++ .../solidify/service/OrcidClientService.java | 179 ++++++++++++++++++ ...cidService.java => OrcidOAuthService.java} | 141 +------------- .../OrcidSynchronizationSpecification.java | 44 +++++ 9 files changed, 476 insertions(+), 152 deletions(-) create mode 100644 solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java create mode 100644 solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java create mode 100644 solidify-orcid/src/main/java/ch/unige/solidify/repository/OrcidSynchronizationRepository.java create mode 100644 solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidClientService.java rename solidify-orcid/src/main/java/ch/unige/solidify/service/{OrcidService.java => OrcidOAuthService.java} (56%) create mode 100644 solidify-orcid/src/main/java/ch/unige/solidify/specification/OrcidSynchronizationSpecification.java diff --git a/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java b/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java index b645d7369..2399435d6 100644 --- a/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java +++ b/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java @@ -9,12 +9,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. @@ -25,12 +25,12 @@ package ch.unige.solidify.auth.model; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "ORCID information identify inddividuals.") +@Schema(description = "ORCID information identify individuals.") public interface OrcidInfo { // ORCID - public static final String ORCID_PATTERN = "|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}X"; - public static final String ORCID_MESSAGE = "The ORCID should be of the form 1234-1234-1234-1234 or 1234-1234-1234-123X"; + String ORCID_PATTERN = "|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}X"; + String ORCID_MESSAGE = "The ORCID should be of the form 1234-1234-1234-1234 or 1234-1234-1234-123X"; @Schema(description = "The ORCID of the person (Format: xxxx-xxxx-xxxx-xxxx).") String getOrcid(); diff --git a/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java b/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java new file mode 100644 index 000000000..1b3234c1d --- /dev/null +++ b/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java @@ -0,0 +1,120 @@ +/*- + * %%---------------------------------------------------------------------------------------------- + * Solidify Framework - Solidify ORCID Model - OrcidSynchronization.java + * SPDX-License-Identifier: GPL-2.0-or-later + * %----------------------------------------------------------------------------------------------% + * Copyright (C) 2017 - 2024 University of Geneva + * %----------------------------------------------------------------------------------------------% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-2.0.html>. + * ----------------------------------------------------------------------------------------------%% + */ +package ch.unige.solidify.model; + +import static ch.unige.solidify.SolidifyConstants.DB_ID_LENGTH; + +import java.math.BigInteger; +import java.time.OffsetDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import ch.unige.solidify.rest.ResourceNormalized; +import ch.unige.solidify.util.StringTool; + +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +public abstract class OrcidSynchronization extends ResourceNormalized { + + @Schema(description = "The resId of the object synchronized with ORCID") + @NotNull + @Size(max = DB_ID_LENGTH) + @Column(length = DB_ID_LENGTH) + protected String objectId; + + @Schema(description = "The identifier of the person for whom the object is synchronised") + @NotNull + @Size(max = DB_ID_LENGTH) + @Column(length = DB_ID_LENGTH) + protected String personId; + + @Schema(description = "Date on which the item was exported to ORCID profile") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = StringTool.DATE_TIME_FORMAT) + private OffsetDateTime uploadDate; + + @Schema(description = "Date on which the item was imported from ORCID profile") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = StringTool.DATE_TIME_FORMAT) + private OffsetDateTime downloadDate; + + @Schema(description = "The ORCID's work put code") + @NotNull + private BigInteger putCode; + + public abstract String getPersonId(); + + public abstract String getObjectId(); + + public OffsetDateTime getUploadDate() { + return this.uploadDate; + } + + public void setUploadDate(OffsetDateTime uploadDate) { + this.uploadDate = uploadDate; + } + + public OffsetDateTime getDownloadDate() { + return this.downloadDate; + } + + public void setDownloadDate(OffsetDateTime downloadDate) { + this.downloadDate = downloadDate; + } + + public BigInteger getPutCode() { + return this.putCode; + } + + public void setPutCode(BigInteger putCode) { + this.putCode = putCode; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + OrcidSynchronization that = (OrcidSynchronization) o; + return Objects.equals(this.objectId, that.objectId) && Objects.equals(this.personId, that.personId) && Objects.equals( + this.uploadDate, that.uploadDate) && Objects.equals(this.downloadDate, that.downloadDate) && Objects.equals(this.putCode, + that.putCode); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.objectId, this.personId, this.uploadDate, this.downloadDate, this.putCode); + } +} diff --git a/solidify-orcid-model/src/main/java/ch/unige/solidify/model/PersonWithOrcid.java b/solidify-orcid-model/src/main/java/ch/unige/solidify/model/PersonWithOrcid.java index eee8ce020..c60ffebad 100644 --- a/solidify-orcid-model/src/main/java/ch/unige/solidify/model/PersonWithOrcid.java +++ b/solidify-orcid-model/src/main/java/ch/unige/solidify/model/PersonWithOrcid.java @@ -9,12 +9,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. @@ -38,4 +38,9 @@ public interface PersonWithOrcid extends OrcidInfo { void setVerifiedOrcid(Boolean verifiedOrcid); void setOrcidToken(OrcidToken orcidToken); + + OrcidToken getOrcidToken(); + + @Schema(description = "The full name of the person", accessMode = Schema.AccessMode.READ_ONLY) + String getFullName(); } diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java b/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java new file mode 100644 index 000000000..a01d04448 --- /dev/null +++ b/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java @@ -0,0 +1,64 @@ +/*- + * %%---------------------------------------------------------------------------------------------- + * Solidify Framework - Solidify ORCID - OrcidSynchronizationService.java + * SPDX-License-Identifier: GPL-2.0-or-later + * %----------------------------------------------------------------------------------------------% + * Copyright (C) 2017 - 2024 University of Geneva + * %----------------------------------------------------------------------------------------------% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-2.0.html>. + * ----------------------------------------------------------------------------------------------%% + */ +package ch.unige.solidify.business; + +import java.math.BigInteger; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; + +import ch.unige.solidify.model.OrcidSynchronization; +import ch.unige.solidify.repository.OrcidSynchronizationRepository; +import ch.unige.solidify.service.OrcidOAuthService; +import ch.unige.solidify.service.ResourceService; +import ch.unige.solidify.specification.OrcidSynchronizationSpecification; +import ch.unige.solidify.specification.SolidifySpecification; + +public abstract class OrcidSynchronizationService extends ResourceService<OrcidSynchronization> { + + protected OrcidOAuthService orcidOAuthService; + + public OrcidSynchronizationService(OrcidOAuthService orcidOAuthService) { + this.orcidOAuthService = orcidOAuthService; + } + + public List<OrcidSynchronization> findByObjectId(String aipId) { + return ((OrcidSynchronizationRepository) this.itemRepository).findByObjectId(aipId); + } + + public List<OrcidSynchronization> findByPersonIdAndObjectId(String personId, String aipId) { + return ((OrcidSynchronizationRepository) this.itemRepository).findByPersonIdAndObjectId(personId, aipId); + } + + public Optional<OrcidSynchronization> findByPersonIdAndPutCode(String personId, BigInteger putCode) { + return ((OrcidSynchronizationRepository) this.itemRepository).findByPersonIdAndPutCode(personId, putCode); + } + + protected abstract OrcidSynchronization storeOrcidSynchronization(String personId, BigInteger putCode, String objectId, + OffsetDateTime uploadDate); + + @Override + public SolidifySpecification<OrcidSynchronization> getSpecification(OrcidSynchronization orcidSynchronization) { + return new OrcidSynchronizationSpecification(orcidSynchronization); + } +} diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/controller/OrcidController.java b/solidify-orcid/src/main/java/ch/unige/solidify/controller/OrcidController.java index 718294832..14640df54 100644 --- a/solidify-orcid/src/main/java/ch/unige/solidify/controller/OrcidController.java +++ b/solidify-orcid/src/main/java/ch/unige/solidify/controller/OrcidController.java @@ -9,12 +9,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. @@ -38,7 +38,7 @@ import ch.unige.solidify.SolidifyConstants; import ch.unige.solidify.rest.ActionName; import ch.unige.solidify.security.EveryonePermissions; import ch.unige.solidify.security.UserPermissions; -import ch.unige.solidify.service.OrcidService; +import ch.unige.solidify.service.OrcidOAuthService; import ch.unige.solidify.util.StringTool; @RestController @@ -47,10 +47,10 @@ import ch.unige.solidify.util.StringTool; public class OrcidController { private static final String LOCATION_HEADER = "Location"; - private final OrcidService orcidService; + private final OrcidOAuthService orcidOAuthService; - public OrcidController(OrcidService orcidService) { - this.orcidService = orcidService; + public OrcidController(OrcidOAuthService orcidOAuthService) { + this.orcidOAuthService = orcidOAuthService; } @EveryonePermissions @@ -61,9 +61,9 @@ public class OrcidController { @RequestParam String authTransactionId) throws JsonProcessingException { String redirectUrl; if (!StringTool.isNullOrEmpty(code)) { - redirectUrl = this.orcidService.getOrcidAndTokensFromOrcidWebsite(code, authTransactionId); + redirectUrl = this.orcidOAuthService.getOrcidAndTokensFromOrcidWebsite(code, authTransactionId); } else { - redirectUrl = this.orcidService.getAndRemoveOriginUrl(authTransactionId, error, errorDescription); + redirectUrl = this.orcidOAuthService.getAndRemoveOriginUrl(authTransactionId, error, errorDescription); } return ResponseEntity.status(HttpStatus.FOUND).header(LOCATION_HEADER, redirectUrl).build(); @@ -72,7 +72,7 @@ public class OrcidController { @UserPermissions @GetMapping("/" + ActionName.START_ORCID_AUTH) public ResponseEntity<String> startOrcidAuth(@RequestParam String originUrl) { - final JSONObject jsonObject = this.orcidService.startOrcidAuth(originUrl); + final JSONObject jsonObject = this.orcidOAuthService.startOrcidAuth(originUrl); return ResponseEntity.ok().body(jsonObject.toString()); } } diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/repository/OrcidSynchronizationRepository.java b/solidify-orcid/src/main/java/ch/unige/solidify/repository/OrcidSynchronizationRepository.java new file mode 100644 index 000000000..20f0476a0 --- /dev/null +++ b/solidify-orcid/src/main/java/ch/unige/solidify/repository/OrcidSynchronizationRepository.java @@ -0,0 +1,43 @@ +/*- + * %%---------------------------------------------------------------------------------------------- + * Solidify Framework - Solidify ORCID - OrcidSynchronizationRepository.java + * SPDX-License-Identifier: GPL-2.0-or-later + * %----------------------------------------------------------------------------------------------% + * Copyright (C) 2017 - 2024 University of Geneva + * %----------------------------------------------------------------------------------------------% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-2.0.html>. + * ----------------------------------------------------------------------------------------------%% + */ +package ch.unige.solidify.repository; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Repository; + +import ch.unige.solidify.controller.OrcidController; +import ch.unige.solidify.model.OrcidSynchronization; + +@Repository +@ConditionalOnBean(OrcidController.class) +public interface OrcidSynchronizationRepository extends SolidifyRepository<OrcidSynchronization> { + List<OrcidSynchronization> findByObjectId(String aipId); + + List<OrcidSynchronization> findByPersonIdAndObjectId(String personId, String aipId); + + Optional<OrcidSynchronization> findByPersonIdAndPutCode(String personId, BigInteger putCode); +} diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidClientService.java b/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidClientService.java new file mode 100644 index 000000000..a56271c70 --- /dev/null +++ b/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidClientService.java @@ -0,0 +1,179 @@ +/*- + * %%---------------------------------------------------------------------------------------------- + * Solidify Framework - Solidify ORCID - OrcidClientService.java + * SPDX-License-Identifier: GPL-2.0-or-later + * %----------------------------------------------------------------------------------------------% + * Copyright (C) 2017 - 2024 University of Geneva + * %----------------------------------------------------------------------------------------------% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-2.0.html>. + * ----------------------------------------------------------------------------------------------%% + */ +package ch.unige.solidify.service; + +import java.io.StringWriter; +import java.math.BigInteger; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; + +import ch.unige.solidify.config.SolidifyProperties; +import ch.unige.solidify.controller.OrcidController; +import ch.unige.solidify.exception.SolidifyResourceNotFoundException; +import ch.unige.solidify.exception.SolidifyRuntimeException; +import ch.unige.solidify.model.OrcidToken; +import ch.unige.solidify.model.xml.orcid.v3_0.activities.Works; +import ch.unige.solidify.model.xml.orcid.v3_0.bulk.Bulk; +import ch.unige.solidify.model.xml.orcid.v3_0.work.Work; + +@Service +@ConditionalOnBean(OrcidController.class) +public class OrcidClientService { + + private static final Logger log = LoggerFactory.getLogger(OrcidClientService.class); + + private static final String BEARER = "Bearer"; + private static final String PUT_CODE_PARAMETER = "put_code"; + private static final String ORCID_PARAMETER = "orcid"; + + private final String apiUrl; + + public OrcidClientService(SolidifyProperties solidifyProperties) { + this.apiUrl = solidifyProperties.getOrcid().getApiBaseUrl(); + } + + public Works getWorks(OrcidToken orcidToken) { + try { + RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + RestTemplate client = restTemplateBuilder.rootUri(this.apiUrl).build(); + + Map<String, String> parameters = new HashMap<>(); + parameters.put(ORCID_PARAMETER, orcidToken.getOrcid()); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, BEARER + " " + orcidToken.getAccessToken()); + HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(headers); + + return client.exchange(this.apiUrl + "{orcid}/works", HttpMethod.GET, entity, Works.class, parameters).getBody(); + } catch (RuntimeException e) { + log.error("An error occurred while getting list of Works from ORCID", e); + return null; + } + } + + public Work getWork(BigInteger putCode, OrcidToken orcidToken) { + try { + RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + RestTemplate client = restTemplateBuilder.rootUri(this.apiUrl).build(); + + Map<String, String> parameters = new HashMap<>(); + parameters.put(ORCID_PARAMETER, orcidToken.getOrcid()); + parameters.put(PUT_CODE_PARAMETER, putCode.toString()); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, BEARER + " " + orcidToken.getAccessToken()); + HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(headers); + + final Bulk bulk = client.exchange(this.apiUrl + "{orcid}/works/{put_code}", HttpMethod.GET, entity, Bulk.class, + parameters).getBody(); + if (bulk != null && !bulk.getWorkOrError().isEmpty() && bulk.getWorkOrError().get(0) instanceof Work work) { + return work; + } + throw new SolidifyResourceNotFoundException("Work with putCode " + putCode + " could not be found on ORCID API"); + } catch (SolidifyResourceNotFoundException e) { + throw e; + } catch (Exception e) { + log.error("An error occurred while getting a Work from ORCID", e); + return null; + } + } + + public BigInteger uploadWork(Work work, OrcidToken orcidToken) { + return this.uploadWork(work, orcidToken, null); + } + + public BigInteger uploadWork(Work work, OrcidToken orcidToken, BigInteger putCode) { + try { + final RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + final RestTemplate client = restTemplateBuilder.rootUri(this.apiUrl).build(); + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, BEARER + " " + orcidToken.getAccessToken()); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML + ";charset=UTF-8"); + if (putCode != null) { + return this.updateOrcidRecord(work, putCode, client, headers, orcidToken); + } else { + return this.createOrcidRecord(work, client, headers, orcidToken); + } + } catch (RuntimeException e) { + throw new SolidifyRuntimeException("An error occurred while uploading a Work to ORCID", e); + } + } + + private BigInteger updateOrcidRecord(Work work, BigInteger putCode, RestTemplate client, HttpHeaders headers, OrcidToken orcidToken) { + work.setPutCode(putCode); + final String workAsXml = this.serializeWorkToXml(work); + final HttpEntity<String> request = new HttpEntity<>(workAsXml, headers); + try { + client.put(this.apiUrl + orcidToken.getOrcid() + "/work/" + putCode, request, String.class); + } catch (HttpClientErrorException.NotFound e) { + // ORCID record has been removed from the ORCID servers, create a new one + work.setPutCode(null); + return this.createOrcidRecord(work, client, headers, orcidToken); + } + return putCode; + } + + private BigInteger createOrcidRecord(Work work, RestTemplate client, HttpHeaders headers, OrcidToken orcidToken) { + // Create ORCID record + final String workAsXml = this.serializeWorkToXml(work); + final HttpEntity<String> request = new HttpEntity<>(workAsXml, headers); + URI locationUri = client.postForEntity(this.apiUrl + orcidToken.getOrcid() + "/work", request, String.class).getHeaders().getLocation(); + if (locationUri != null) { + String path = locationUri.getPath(); + return BigInteger.valueOf(Long.parseLong(path.substring(path.lastIndexOf("/") + 1))); + } else { + return null; + } + } + + private String serializeWorkToXml(Work work) { + try { + final JAXBContext jaxbContext = JAXBContext.newInstance(Work.class); + final Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); + jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + StringWriter sw = new StringWriter(); + jaxbMarshaller.marshal(work, sw); + return sw.toString(); + } catch (RuntimeException | JAXBException e) { + throw new SolidifyRuntimeException("Unable to serialize ORCID Work to XML", e); + } + } +} diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidService.java b/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidOAuthService.java similarity index 56% rename from solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidService.java rename to solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidOAuthService.java index 9d6fa6a6a..6701219da 100644 --- a/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidService.java +++ b/solidify-orcid/src/main/java/ch/unige/solidify/service/OrcidOAuthService.java @@ -1,6 +1,6 @@ /*- * %%---------------------------------------------------------------------------------------------- - * Solidify Framework - Solidify ORCID - OrcidService.java + * Solidify Framework - Solidify ORCID - OrcidOAuthService.java * SPDX-License-Identifier: GPL-2.0-or-later * %----------------------------------------------------------------------------------------------% * Copyright (C) 2017 - 2024 University of Geneva @@ -23,9 +23,6 @@ package ch.unige.solidify.service; -import java.io.StringWriter; -import java.math.BigInteger; -import java.net.URI; import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; @@ -35,17 +32,9 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; @@ -53,34 +42,24 @@ import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; -import jakarta.xml.bind.Marshaller; import reactor.core.publisher.Mono; import ch.unige.solidify.config.SolidifyProperties; import ch.unige.solidify.controller.OrcidController; -import ch.unige.solidify.exception.SolidifyResourceNotFoundException; -import ch.unige.solidify.exception.SolidifyRuntimeException; import ch.unige.solidify.model.OrcidToken; import ch.unige.solidify.model.PersonWithOrcid; -import ch.unige.solidify.model.xml.orcid.v3_0.activities.Works; -import ch.unige.solidify.model.xml.orcid.v3_0.bulk.Bulk; -import ch.unige.solidify.model.xml.orcid.v3_0.work.Work; @Service @ConditionalOnBean(OrcidController.class) -public class OrcidService { +public class OrcidOAuthService { - private static final Logger log = LoggerFactory.getLogger(OrcidService.class); + private static final Logger log = LoggerFactory.getLogger(OrcidOAuthService.class); private static final String ORCID_PARAMETER = "orcid"; - private static final String PUT_CODE_PARAMETER = "put_code"; + private static final String ORCID_ERROR_PARAMETER = "orcid_error"; private static final String ORCID_ERROR_DESCRIPTION_PARAMETER = "orcid_error_description"; - private static final String BEARER = "Bearer"; - private final PersonWithOrcidService personWithOrcidService; private final String grantType; @@ -89,17 +68,14 @@ public class OrcidService { private final String clientId; private final String clientSecret; - private final String apiUrl; - private final Map<String, PersonOrigin> mapPersonOrigin = new HashMap<>(); - public OrcidService(PersonWithOrcidService personWithOrcidService, SolidifyProperties solidifyProperties) { + public OrcidOAuthService(PersonWithOrcidService personWithOrcidService, SolidifyProperties solidifyProperties) { this.personWithOrcidService = personWithOrcidService; this.clientId = solidifyProperties.getOrcid().getClientId(); this.clientSecret = solidifyProperties.getOrcid().getClientSecret(); this.oAuthTokenUrl = solidifyProperties.getOrcid().getTokenUrl(); this.grantType = solidifyProperties.getOrcid().getGrantType(); - this.apiUrl = solidifyProperties.getOrcid().getApiBaseUrl(); } private PersonWithOrcid getCurrentPerson() { @@ -190,113 +166,6 @@ public class OrcidService { return uriBuilder.toUriString(); } - public Works getWorks(OrcidToken orcidToken) { - try { - RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); - RestTemplate client = restTemplateBuilder.rootUri(this.apiUrl).build(); - - Map<String, String> parameters = new HashMap<>(); - parameters.put(ORCID_PARAMETER, orcidToken.getOrcid()); - - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.AUTHORIZATION, BEARER + " " + orcidToken.getAccessToken()); - HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(headers); - - return client.exchange(this.apiUrl + "{orcid}/works", HttpMethod.GET, entity, Works.class, parameters).getBody(); - } catch (RuntimeException e) { - log.error("An error occurred while getting list of Works from ORCID", e); - return null; - } - } - - public Work getWork(BigInteger putCode, OrcidToken orcidToken) { - try { - RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); - RestTemplate client = restTemplateBuilder.rootUri(this.apiUrl).build(); - - Map<String, String> parameters = new HashMap<>(); - parameters.put(ORCID_PARAMETER, orcidToken.getOrcid()); - parameters.put(PUT_CODE_PARAMETER, putCode.toString()); - - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.AUTHORIZATION, BEARER + " " + orcidToken.getAccessToken()); - HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(headers); - - final Bulk bulk = client.exchange(this.apiUrl + "{orcid}/works/{put_code}", HttpMethod.GET, entity, Bulk.class, - parameters).getBody(); - if (bulk != null && !bulk.getWorkOrError().isEmpty() && bulk.getWorkOrError().get(0) instanceof Work work) { - return work; - } - throw new SolidifyResourceNotFoundException("Work with putCode " + putCode + " could not be found on ORCID API"); - } catch (SolidifyResourceNotFoundException e) { - throw e; - } catch (Exception e) { - log.error("An error occurred while getting a Work from ORCID", e); - return null; - } - } - - public BigInteger uploadWork(Work work, OrcidToken orcidToken) { - return uploadWork(work, orcidToken, null); - } - - public BigInteger uploadWork(Work work, OrcidToken orcidToken, BigInteger putCode) { - try { - final RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); - final RestTemplate client = restTemplateBuilder.rootUri(this.apiUrl).build(); - final HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.AUTHORIZATION, BEARER + " " + orcidToken.getAccessToken()); - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML + ";charset=UTF-8"); - if (putCode != null) { - return this.updateOrcidRecord(work, putCode, client, headers, orcidToken); - } else { - return this.createOrcidRecord(work, client, headers, orcidToken); - } - } catch (RuntimeException e) { - throw new SolidifyRuntimeException("An error occurred while uploading a Work to ORCID", e); - } - } - - private BigInteger updateOrcidRecord(Work work, BigInteger putCode, RestTemplate client, HttpHeaders headers, OrcidToken orcidToken) { - work.setPutCode(putCode); - final String workAsXml = this.serializeWorkToXml(work); - final HttpEntity<String> request = new HttpEntity<>(workAsXml, headers); - try { - client.put(this.apiUrl + orcidToken.getOrcid() + "/work/" + putCode, request, String.class); - } catch (HttpClientErrorException.NotFound e) { - // ORCID record has been removed from the ORCID servers, create a new one - work.setPutCode(null); - return this.createOrcidRecord(work, client, headers, orcidToken); - } - return putCode; - } - - private BigInteger createOrcidRecord(Work work, RestTemplate client, HttpHeaders headers, OrcidToken orcidToken) { - // Create ORCID record - final String workAsXml = this.serializeWorkToXml(work); - final HttpEntity<String> request = new HttpEntity<>(workAsXml, headers); - URI locationUri = client.postForEntity(this.apiUrl + orcidToken.getOrcid() + "/work", request, String.class).getHeaders().getLocation(); - if (locationUri != null) { - String path = locationUri.getPath(); - return BigInteger.valueOf(Long.parseLong(path.substring(path.lastIndexOf("/") + 1))); - } else { - return null; - } - } - - private String serializeWorkToXml(Work work) { - try { - final JAXBContext jaxbContext = JAXBContext.newInstance(Work.class); - final Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); - jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - StringWriter sw = new StringWriter(); - jaxbMarshaller.marshal(work, sw); - return sw.toString(); - } catch (RuntimeException | JAXBException e) { - throw new SolidifyRuntimeException("Unable to serialize ORCID Work to XML", e); - } - } - private static class PersonOrigin { private final String originUrl; private final String personId; diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/specification/OrcidSynchronizationSpecification.java b/solidify-orcid/src/main/java/ch/unige/solidify/specification/OrcidSynchronizationSpecification.java new file mode 100644 index 000000000..e3aa962e8 --- /dev/null +++ b/solidify-orcid/src/main/java/ch/unige/solidify/specification/OrcidSynchronizationSpecification.java @@ -0,0 +1,44 @@ +/*- + * %%---------------------------------------------------------------------------------------------- + * Solidify Framework - Solidify ORCID - OrcidSynchronizationSpecification.java + * SPDX-License-Identifier: GPL-2.0-or-later + * %----------------------------------------------------------------------------------------------% + * Copyright (C) 2017 - 2024 University of Geneva + * %----------------------------------------------------------------------------------------------% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-2.0.html>. + * ----------------------------------------------------------------------------------------------%% + */ +package ch.unige.solidify.specification; + +import java.util.List; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import ch.unige.solidify.model.OrcidSynchronization; + +public class OrcidSynchronizationSpecification extends SolidifySpecification<OrcidSynchronization> { + public OrcidSynchronizationSpecification(OrcidSynchronization criteria) { + super(criteria); + } + + @Override + protected void completePredicatesList(Root<OrcidSynchronization> root, CriteriaQuery<?> query, CriteriaBuilder builder, + List<Predicate> predicatesList) { + // do nothing + } +} -- GitLab From 370179538e8958e2ce1464c758d6f8c5a936032f Mon Sep 17 00:00:00 2001 From: Nicolas Rod <Nicolas.Rod@unige.ch> Date: Thu, 23 Jan 2025 18:35:25 +0100 Subject: [PATCH 2/4] solidify sendToOrcidProfile() method --- .../solidify/model/OrcidSynchronization.java | 11 +++ .../business/OrcidSynchronizationService.java | 72 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java b/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java index 1b3234c1d..eca28a22b 100644 --- a/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java +++ b/solidify-orcid-model/src/main/java/ch/unige/solidify/model/OrcidSynchronization.java @@ -23,6 +23,8 @@ package ch.unige.solidify.model; import static ch.unige.solidify.SolidifyConstants.DB_ID_LENGTH; +import static ch.unige.solidify.model.OrcidSynchronization.OBJECT_ID_DB_FIELD; +import static ch.unige.solidify.model.OrcidSynchronization.PERSON_ID_DB_FIELD; import java.math.BigInteger; import java.time.OffsetDateTime; @@ -34,6 +36,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -42,8 +46,15 @@ import ch.unige.solidify.util.StringTool; @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@Table(name = "orcid_synchronization", uniqueConstraints = { + @UniqueConstraint(columnNames = { OBJECT_ID_DB_FIELD, PERSON_ID_DB_FIELD, "put_code" }) }) public abstract class OrcidSynchronization extends ResourceNormalized { + public static final String OBJECT_ID_DB_FIELD = "object_id"; + public static final String OBJECT_ID_FIELD = "objectId"; + public static final String PERSON_ID_DB_FIELD = "person_id"; + public static final String PERSON_ID_FIELD = "personId"; + @Schema(description = "The resId of the object synchronized with ORCID") @NotNull @Size(max = DB_ID_LENGTH) diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java b/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java index a01d04448..08454c732 100644 --- a/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java +++ b/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java @@ -26,20 +26,37 @@ import java.math.BigInteger; import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; +import javax.xml.namespace.QName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.xml.bind.JAXBElement; + +import ch.unige.solidify.exception.SolidifyRuntimeException; import ch.unige.solidify.model.OrcidSynchronization; +import ch.unige.solidify.model.PersonWithOrcid; +import ch.unige.solidify.model.xml.orcid.v3_0.common.OrcidId; +import ch.unige.solidify.model.xml.orcid.v3_0.work.Work; import ch.unige.solidify.repository.OrcidSynchronizationRepository; -import ch.unige.solidify.service.OrcidOAuthService; +import ch.unige.solidify.service.OrcidClientService; import ch.unige.solidify.service.ResourceService; import ch.unige.solidify.specification.OrcidSynchronizationSpecification; import ch.unige.solidify.specification.SolidifySpecification; public abstract class OrcidSynchronizationService extends ResourceService<OrcidSynchronization> { - protected OrcidOAuthService orcidOAuthService; + private static final Logger log = LoggerFactory.getLogger(OrcidSynchronizationService.class); + + protected static final String ORCID_COMMON_NS = "http://www.orcid.org/ns/common"; + protected static final String ORCID_DATASET_TYPE = "data-set"; + protected static final String ORCID_DOI_TYPE = "doi"; + protected static final String ORCID_SELF_RELATIONSHIP = "self"; - public OrcidSynchronizationService(OrcidOAuthService orcidOAuthService) { - this.orcidOAuthService = orcidOAuthService; + protected final OrcidClientService orcidClientService; + + public OrcidSynchronizationService(OrcidClientService orcidClientService) { + this.orcidClientService = orcidClientService; } public List<OrcidSynchronization> findByObjectId(String aipId) { @@ -54,8 +71,51 @@ public abstract class OrcidSynchronizationService extends ResourceService<OrcidS return ((OrcidSynchronizationRepository) this.itemRepository).findByPersonIdAndPutCode(personId, putCode); } - protected abstract OrcidSynchronization storeOrcidSynchronization(String personId, BigInteger putCode, String objectId, - OffsetDateTime uploadDate); + public OrcidSynchronization sendToOrcidProfile(PersonWithOrcid person, String objectId) { + if (person.getOrcidToken() == null) { + throw new SolidifyRuntimeException("Person " + person.getFullName() + " (" + person.getResId() + ") doesn't have any ORCID token"); + } + final Work work = this.buildWork(objectId); + final List<OrcidSynchronization> previousSynchronizationList = this.findByPersonIdAndObjectId(person.getResId(), objectId); + if (previousSynchronizationList.isEmpty()) { + // Create ORCID record + BigInteger putCode = this.orcidClientService.uploadWork(work, person.getOrcidToken()); + log.info("Object '{}' ({}) has been uploaded to ORCID profile of person {} as Work with putCode {}", work.getTitle().getTitle(), objectId, + person, putCode); + return this.saveOrcidSynchronization(person.getResId(), putCode, objectId, OffsetDateTime.now()); + } else if (previousSynchronizationList.size() == 1) { + // Update ORCID record + final OrcidSynchronization previousSynchronization = previousSynchronizationList.get(0); + BigInteger putCode = this.orcidClientService.uploadWork(work, person.getOrcidToken(), previousSynchronization.getPutCode()); + log.info("Object '{}' ({}) has been updated on ORCID profile of person {} as Work with putCode {}", work.getTitle().getTitle(), objectId, + person, putCode); + if (putCode != null) { + previousSynchronization.setPutCode(putCode); + } + previousSynchronization.setUploadDate(OffsetDateTime.now()); + return this.save(previousSynchronization); + } else { + throw new IllegalStateException( + "More than one OrcidSynchronization found for person " + person + " for object '" + work.getTitle().getTitle() + "' (" + objectId + + ")"); + } + } + + public abstract OrcidSynchronization saveOrcidSynchronization(String personId, BigInteger putCode, String objectId, OffsetDateTime uploadDate); + + protected abstract Work buildWork(String objectId); + + protected OrcidId buildOrcidId(String orcidValue) { + OrcidId orcidId = new OrcidId(); + JAXBElement<String> uri = new JAXBElement<>(new QName(ORCID_COMMON_NS, "uri"), String.class, + "https://orcid.org/" + orcidValue); + orcidId.getContent().add(uri); + JAXBElement<String> path = new JAXBElement<>(new QName(ORCID_COMMON_NS, "path"), String.class, orcidValue); + orcidId.getContent().add(path); + JAXBElement<String> host = new JAXBElement<>(new QName(ORCID_COMMON_NS, "host"), String.class, "orcid.org"); + orcidId.getContent().add(host); + return orcidId; + } @Override public SolidifySpecification<OrcidSynchronization> getSpecification(OrcidSynchronization orcidSynchronization) { -- GitLab From f4eea95cbc8aeb032adbe0ffdbf18c51aae8476a Mon Sep 17 00:00:00 2001 From: Nicolas Rod <Nicolas.Rod@unige.ch> Date: Fri, 24 Jan 2025 12:33:15 +0100 Subject: [PATCH 3/4] improve log message when update is replaced by create --- .../business/OrcidSynchronizationService.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java b/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java index 08454c732..5afd16b7d 100644 --- a/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java +++ b/solidify-orcid/src/main/java/ch/unige/solidify/business/OrcidSynchronizationService.java @@ -86,9 +86,15 @@ public abstract class OrcidSynchronizationService extends ResourceService<OrcidS } else if (previousSynchronizationList.size() == 1) { // Update ORCID record final OrcidSynchronization previousSynchronization = previousSynchronizationList.get(0); - BigInteger putCode = this.orcidClientService.uploadWork(work, person.getOrcidToken(), previousSynchronization.getPutCode()); - log.info("Object '{}' ({}) has been updated on ORCID profile of person {} as Work with putCode {}", work.getTitle().getTitle(), objectId, - person, putCode); + BigInteger previousPutCode = previousSynchronization.getPutCode(); + BigInteger putCode = this.orcidClientService.uploadWork(work, person.getOrcidToken(), previousPutCode); + if (previousPutCode.equals(putCode)) { + log.info("Object '{}' ({}) has been updated on ORCID profile of person {} as Work with putCode {}", work.getTitle().getTitle(), objectId, + person, putCode); + } else { + log.info("Object '{}' ({}) has been created again on ORCID profile of person {} as Work with putCode {}", work.getTitle().getTitle(), + objectId, person, putCode); + } if (putCode != null) { previousSynchronization.setPutCode(putCode); } -- GitLab From 1a68eb34ca2d479ce388dadc8fff6cad52cf1189 Mon Sep 17 00:00:00 2001 From: Nicolas Rod <Nicolas.Rod@unige.ch> Date: Fri, 24 Jan 2025 16:09:09 +0100 Subject: [PATCH 4/4] revert public static final --- .../src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java b/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java index 2399435d6..5fad8d09e 100644 --- a/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java +++ b/solidify-authorization-model/src/main/java/ch/unige/solidify/auth/model/OrcidInfo.java @@ -29,8 +29,8 @@ import io.swagger.v3.oas.annotations.media.Schema; public interface OrcidInfo { // ORCID - String ORCID_PATTERN = "|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}X"; - String ORCID_MESSAGE = "The ORCID should be of the form 1234-1234-1234-1234 or 1234-1234-1234-123X"; + public static final String ORCID_PATTERN = "|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}X"; + public static final String ORCID_MESSAGE = "The ORCID should be of the form 1234-1234-1234-1234 or 1234-1234-1234-123X"; @Schema(description = "The ORCID of the person (Format: xxxx-xxxx-xxxx-xxxx).") String getOrcid(); -- GitLab