From df312eee7aa0976656bb280539c6580052945878 Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Mon, 4 May 2026 13:38:10 +0200 Subject: [PATCH 1/4] Issue 1275: replace PIDServiceEPICV2 with EpicHAndleService --- .../main/java/org/dspace/api/DSpaceApi.java | 132 ------ .../org/dspace/handle/AbstractPIDService.java | 91 ---- .../org/dspace/handle/EpicHandleField.java | 21 + .../dspace/handle/EpicHandleRestHelper.java | 40 +- .../dspace/handle/EpicHandleServiceImpl.java | 42 +- .../java/org/dspace/handle/HandlePlugin.java | 66 +-- .../org/dspace/handle/HandleServiceImpl.java | 105 ++++- .../java/org/dspace/handle/PIDService.java | 133 ------ .../org/dspace/handle/PIDServiceEPICv2.java | 409 ------------------ .../handle/service/EpicHandleService.java | 37 ++ .../dspace/handle/PIDConfigurationTest.java | 5 +- dspace/config/clarin-dspace.cfg | 4 +- dspace/config/spring/api/core-services.xml | 7 +- 13 files changed, 260 insertions(+), 832 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/api/DSpaceApi.java delete mode 100644 dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/EpicHandleField.java delete mode 100644 dspace-api/src/main/java/org/dspace/handle/PIDService.java delete mode 100644 dspace-api/src/main/java/org/dspace/handle/PIDServiceEPICv2.java diff --git a/dspace-api/src/main/java/org/dspace/api/DSpaceApi.java b/dspace-api/src/main/java/org/dspace/api/DSpaceApi.java deleted file mode 100644 index 34b5acff59f9..000000000000 --- a/dspace-api/src/main/java/org/dspace/api/DSpaceApi.java +++ /dev/null @@ -1,132 +0,0 @@ -/** - * 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/ - */ - - -/* Created for LINDAT/CLARIAH-CZ (UFAL) */ - -package org.dspace.api; - -import java.io.IOException; -import java.sql.SQLException; -import java.util.Map; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.dspace.content.DSpaceObject; -import org.dspace.handle.HandlePlugin; -import org.dspace.handle.PIDService; -import org.dspace.services.ConfigurationService; -import org.dspace.utils.DSpace; - -public class DSpaceApi { - - private static final org.apache.logging.log4j.Logger log = LogManager.getLogger(); - - private static ConfigurationService configurationService = new DSpace().getConfigurationService(); - - private DSpaceApi() { - - } - /** - * Create a new handle PID. This is modified implementation for UFAL, using - * the PID service pidconsortium.eu as wrapped in the PIDService class. - * - * Note: this function creates a handle to a provisional existing URL and - * the handle must be updated to point to the final URL once DSpace is able - * to report the URL exists (otherwise the pidservice will refuse to set the - * URL) - * - * @return A new handle PID - * @exception Exception If error occurrs - */ - public static String handle_HandleManager_createId(Logger log, Long id, - String prefix, String suffix) throws IOException { - - /* Modified by PP for use pidconsortium.eu at UFAL/CLARIN */ - - String base_url = configurationService.getProperty("dspace.server.url") + "?dummy=" + id; - - /* OK check whether this url has not received pid earlier */ - //This should usually return null (404) - String handle = null; - try { - handle = PIDService.findHandle(base_url, prefix); - } catch (Exception e) { - log.error("Error finding handle: " + e); - } - //if not then log and reuse - this is a dummy url, those should not be seen anywhere - if (handle != null) { - log.warn("Url [" + base_url + "] already has PID(s) (" + handle + ")."); - return handle; - } - /* /OK/ */ - - log.debug("Asking for a new PID using a dummy URL " + base_url); - - /* request a new PID, initially pointing to dspace base_uri+id */ - String pid = null; - try { - if (suffix != null && !suffix.isEmpty() && PIDService.supportsCustomPIDs()) { - pid = PIDService.createCustomPID(base_url, prefix, suffix); - } else { - pid = PIDService.createPID(base_url, prefix); - } - } catch (Exception e) { - throw new IOException(e); - } - - log.debug("got PID " + pid); - return pid; - } - - /** - * Modify an existing PID to point to the corresponding DSpace handle - * - * @exception SQLException If a database error occurs - */ - public static void handle_HandleManager_registerFinalHandleURL(Logger log, - String pid, DSpaceObject dso) throws IOException { - if (pid == null) { - log.info("Modification failed invalid/null PID."); - return; - } - - String url = generateItemURLWithHandle(pid, dso); - - /* - * request modification of the PID to point to the correct URL, which - * itself should contain the PID as a substring - */ - log.debug("Asking for changing the PID '" + pid + "' to " + url); - - try { - Map fields = HandlePlugin.extractMetadata(dso); - PIDService.modifyPID(pid, url, fields); - } catch (Exception e) { - throw new IOException("Failed to map PID " + pid + " to " + url - + " (" + e.toString() + ")"); - } - } - - /** - * Generate a URL with the handle for the given DSpaceObject. - * The URL consist of the base URL and the handle of the DSpace object. - * E.g. `http://localhost:4000/handle/` - * @param pid the pid we are using (obtained e.g. from EPIC) - * @param dSpaceObject the DSpace object for which the URL is generated - * @return the generated URL - */ - public static String generateItemURLWithHandle(String pid, DSpaceObject dSpaceObject) { - String url = configurationService.getProperty("dspace.ui.url"); - if (dSpaceObject == null) { - log.error("DSpaceObject is null, cannot generate URL"); - return url; - } - return url + (url.endsWith("/") ? "" : "/") + "handle/" + pid; - } -} diff --git a/dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java b/dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java deleted file mode 100644 index a1c3c75185c1..000000000000 --- a/dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * 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.handle; - -import java.net.Authenticator; -import java.net.PasswordAuthentication; -import java.util.Map; - -import org.dspace.services.ConfigurationService; -import org.dspace.utils.DSpace; -import org.springframework.stereotype.Component; - -/* Created for LINDAT/CLARIAH-CZ (UFAL) */ -/** - * Abstract class for PID service which manages EPIC handles. - * This class loads the parameters from configuration for calling EPIC API. - * - * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) - */ -@Component -public abstract class AbstractPIDService { - public String PIDServiceURL; - public String PIDServiceUSER; - public String PIDServicePASS; - - private ConfigurationService configurationService = new DSpace().getConfigurationService(); - - class PIDServiceAuthenticator extends Authenticator { - public PasswordAuthentication getPasswordAuthentication() { - return (new PasswordAuthentication(PIDServiceUSER, - PIDServicePASS.toCharArray())); - } - } - - public enum HTTPMethod { - GET, POST, PUT, DELETE - } - - public enum PARAMS { - PID, DATA, COMMAND, REGEX, HEADER - } - - public enum HANDLE_FIELDS { - URL, - TITLE, - REPOSITORY, - SUBMITDATE, - REPORTEMAIL, - DATASETNAME, - DATASETVERSION, - QUERY - } - - public PIDServiceAuthenticator authenticator = null; - - public AbstractPIDService() throws Exception { - PIDServiceURL = configurationService.getProperty("lr.pid.service.url", "lr.pid.service.url"); - PIDServiceUSER = configurationService.getProperty("lr.pid.service.user", "lr.pid.service.user"); - PIDServicePASS = configurationService.getProperty("lr.pid.service.pass", "lr.pid.service.pass"); - if (PIDServiceURL == null || PIDServiceURL.length() == 0) { - throw new Exception("PIDService URL not configured."); - } - authenticator = new PIDServiceAuthenticator(); - Authenticator.setDefault(authenticator); - } - - public abstract String sendPIDCommand(HTTPMethod method, Map params) throws Exception; - - public abstract String resolvePID(String PID) throws Exception; - - public abstract String createPID(Map handleFields, String prefix) throws Exception; - - public abstract String createCustomPID(Map handleFields, - String prefix, String suffix) throws Exception; - - public abstract String modifyPID(String PID, Map handleFields) throws Exception; - - public abstract String deletePID(String PID) throws Exception; - - public abstract String findHandle(Map handleFields, String prefix) throws Exception; - - public abstract boolean supportsCustomPIDs() throws Exception; - - public abstract String whoAmI(String encoding) throws Exception; - -} diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleField.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleField.java new file mode 100644 index 000000000000..79d6454d80b9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleField.java @@ -0,0 +1,21 @@ +/** + * 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.handle; + +/** + * Enum with fields used for EPIC handle creation and update. These fields are used as keys in the map of parameters + */ +public enum EpicHandleField { + TITLE, + REPOSITORY, + SUBMITDATE, + REPORTEMAIL, + DATASETNAME, + DATASETVERSION, + QUERY +} diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java index 360543af1b1d..947bb6f160c2 100644 --- a/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java @@ -10,6 +10,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Map; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; @@ -56,17 +57,19 @@ public static Response createHandle(String pidServiceURL, .post(Entity.json(jsonData)); } + public static Response createNewHandleWithSuffix(String pidServiceURL, + String prefix, + String suffix, + String jsonData) { + return putHandle(pidServiceURL, prefix, suffix, jsonData, Map.of("If-None-Match", "*")); + } + public static Response updateHandle(String pidServiceURL, String prefix, String suffix, String jsonData) { - URI uri; - try { - uri = new URI(pidServiceURL); - } catch (URISyntaxException e) { - return invalidUriResponse(); - } + return putHandle(pidServiceURL, prefix, suffix, jsonData, null); + } - return client.target(uri).path(prefix).path(suffix) - .request(MediaType.APPLICATION_JSON) - .put(Entity.json(jsonData)); + public static Response updateHandleIfExists(String pidServiceURL, String prefix, String suffix, String jsonData) { + return putHandle(pidServiceURL, prefix, suffix, jsonData, Map.of("If-Match", "*")); } public static Response deleteHandle(String pidServiceURL, String prefix, String suffix) { @@ -137,6 +140,25 @@ public static Response getHandle(String pidServiceURL, String prefix, String suf .get(); } + private static Response putHandle(String pidServiceURL, + String prefix, + String suffix, + String jsonData, + Map headers) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + Invocation.Builder req = client.target(uri).path(prefix).path(suffix).request(MediaType.APPLICATION_JSON); + if (headers != null) { + headers.forEach(req::header); + } + return req.put(Entity.json(jsonData)); + } + private static Response invalidUriResponse() { return Response.status(400, "invalid ePIC PID Service URL").build(); } diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java index 9c67d7b0c1ab..377caf65be9b 100644 --- a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java @@ -123,6 +123,21 @@ public String createHandle(String prefix, String subPrefix, String subSuffix, St } } + @Override + public String createNewHandleWithSuffix(String prefix, String suffix, String url) throws IOException { + initialize(); + String jsonData = getJsonDataForUrl(objectMapper, url).toString(); + + try (Response response = EpicHandleRestHelper.createNewHandleWithSuffix(pidServiceUrl, + prefix, suffix, jsonData)) { + if (response.getStatus() == Response.Status.CREATED.getStatusCode()) { + return objectMapper.readValue(response.readEntity(String.class), EpicPid.class).getHandle(); + } else { + throw new WebApplicationException(response); + } + } + } + @Override public String createOrUpdateHandle(String prefix, String suffix, String url) throws IOException { initialize(); @@ -139,6 +154,18 @@ public String createOrUpdateHandle(String prefix, String suffix, String url) thr } } + @Override + public void updateHandleIfExists(String prefix, String suffix, String url, Map data) + throws IOException { + initialize(); + String jsonData = getJsonData(objectMapper, url, data).toString(); + try (Response response = EpicHandleRestHelper.updateHandleIfExists(pidServiceUrl, prefix, suffix, jsonData)) { + if (response.getStatus() != Response.Status.NO_CONTENT.getStatusCode()) { + throw new WebApplicationException(response); + } + } + } + @Override public void deleteHandle(String prefix, String suffix) throws IOException { initialize(); @@ -166,10 +193,23 @@ public int countHandles(String prefix, String urlQuery) throws IOException { } private static ArrayNode getJsonDataForUrl(ObjectMapper objectMapper, String url) { + return getJsonData(objectMapper, url, Map.of()); + } + + private static ArrayNode getJsonData(ObjectMapper objectMapper, + String url, + Map additionalData) { ObjectNode epicPidDataNode = objectMapper.createObjectNode() .put("type", "URL") .put("parsed_data", url); - return objectMapper.createArrayNode().add(epicPidDataNode); + ArrayNode arrayNode = objectMapper.createArrayNode().add(epicPidDataNode); + additionalData.forEach((key, value) -> { + ObjectNode additionalDataNode = objectMapper.createObjectNode() + .put("type", key.name()) + .put("parsed_data", value); + arrayNode.add(additionalDataNode); + }); + return arrayNode; } private static String getUrlFromEpicDataList(List epicPidDataList) { diff --git a/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java b/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java index a22736c179bb..810bebbad343 100644 --- a/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java +++ b/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import net.cnri.util.StreamTable; import net.handle.hdllib.Encoder; @@ -292,7 +293,8 @@ public static Map getMapHandleValues(String handle) throws Handl if (resolveMetadata) { dso = resolveHandleToObject(context, handle); } - return extractMetadata(dso); + return extractMetadata(dso).entrySet().stream().collect( + Collectors.toMap(e -> e.getKey().name(), Map.Entry::getValue)); } finally { try { context.complete(); @@ -612,8 +614,8 @@ public static String getCanonicalHandlePrefix() { return canonicalHandlePrefix; } - public static Map extractMetadata(DSpaceObject dso) { - Map map = new LinkedHashMap<>(); + public static Map extractMetadata(DSpaceObject dso) { + Map map = new LinkedHashMap<>(); if (Objects.isNull(dso)) { return map; } @@ -627,14 +629,14 @@ public static Map extractMetadata(DSpaceObject dso) { // load the DSpaceObject metadata List mds = itemService.getMetadataByMetadataString((Item) dso, "dc.title"); if (CollectionUtils.isNotEmpty(mds)) { - map.put(AbstractPIDService.HANDLE_FIELDS.TITLE.toString(), mds.get(0).getValue()); + map.put(EpicHandleField.TITLE, mds.get(0).getValue()); } - map.put(AbstractPIDService.HANDLE_FIELDS.REPOSITORY.toString(), getRepositoryName()); + map.put(EpicHandleField.REPOSITORY, getRepositoryName()); mds = itemService.getMetadataByMetadataString((Item) dso, "dc.date.accessioned"); if (CollectionUtils.isNotEmpty(mds)) { - map.put(AbstractPIDService.HANDLE_FIELDS.SUBMITDATE.toString(), mds.get(0).getValue()); + map.put(EpicHandleField.SUBMITDATE, mds.get(0).getValue()); } - map.put(AbstractPIDService.HANDLE_FIELDS.REPORTEMAIL.toString(), getRepositoryEmail()); + map.put(EpicHandleField.REPORTEMAIL, getRepositoryEmail()); return map; } } @@ -656,31 +658,15 @@ public ResolvedHandle(String url, DSpaceObject dso) { String submitdate = null; String reportemail = null; if (null != dso) { - Map map = HandlePlugin.extractMetadata(dso); - String key - = AbstractPIDService.HANDLE_FIELDS.TITLE.toString(); - title = getOrDefault(map, key, ""); - - key = AbstractPIDService.HANDLE_FIELDS.REPOSITORY.toString(); - repository = getOrDefault(map, key, ""); - - key = AbstractPIDService.HANDLE_FIELDS.SUBMITDATE.toString(); - submitdate = getOrDefault(map, key, ""); - - key = AbstractPIDService.HANDLE_FIELDS.REPORTEMAIL.toString(); - reportemail = getOrDefault(map, key, ""); + Map map = HandlePlugin.extractMetadata(dso); + title = map.getOrDefault(EpicHandleField.TITLE, ""); + repository = map.getOrDefault(EpicHandleField.REPOSITORY, ""); + submitdate = map.getOrDefault(EpicHandleField.SUBMITDATE, ""); + reportemail = map.getOrDefault(EpicHandleField.REPORTEMAIL, ""); } init(url, title, repository, submitdate, reportemail); } - private V getOrDefault(Map map, K key, V defaultValue) { - if (map.containsKey(key)) { - return map.get(key); - } else { - return defaultValue; - } - } - private void init(String url, String title, String repository, String submitdate, String reportemail) { init(url, title, repository, submitdate, reportemail, null, null, null); } @@ -702,36 +688,26 @@ private void init(String url, String title, String repository, String submitdate } } setResolvedUrl(url); - String key; if (null != title) { - key = AbstractPIDService.HANDLE_FIELDS.TITLE.toString(); - setValue(key, title); + setValue(EpicHandleField.TITLE.name(), title); } - if (null != repository) { - key = AbstractPIDService.HANDLE_FIELDS.REPOSITORY.toString(); - setValue(key, repository); + setValue(EpicHandleField.REPOSITORY.name(), repository); } - if (null != submitdate) { - key = AbstractPIDService.HANDLE_FIELDS.SUBMITDATE.toString(); - setValue(key, submitdate); + setValue(EpicHandleField.SUBMITDATE.name(), submitdate); } if (null != reportemail) { - key = AbstractPIDService.HANDLE_FIELDS.REPORTEMAIL.toString(); - setValue(key, reportemail); + setValue(EpicHandleField.REPORTEMAIL.name(), reportemail); } if (isNotBlank(datasetName)) { - key = AbstractPIDService.HANDLE_FIELDS.DATASETNAME.toString(); - setValue(key, datasetName); + setValue(EpicHandleField.DATASETNAME.name(), datasetName); } if (isNotBlank(datasetVersion)) { - key = AbstractPIDService.HANDLE_FIELDS.DATASETVERSION.toString(); - setValue(key, datasetVersion); + setValue(EpicHandleField.DATASETVERSION.name(), datasetVersion); } if (isNotBlank(query)) { - key = AbstractPIDService.HANDLE_FIELDS.QUERY.toString(); - setValue(key, query); + setValue(EpicHandleField.QUERY.name(), query); } } diff --git a/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java index af4325e42099..8f140077da16 100644 --- a/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java @@ -15,6 +15,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.regex.Matcher; @@ -25,7 +26,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.dspace.api.DSpaceApi; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -35,6 +35,7 @@ import org.dspace.core.Context; import org.dspace.event.Event; import org.dspace.handle.dao.HandleDAO; +import org.dspace.handle.service.EpicHandleService; import org.dspace.handle.service.HandleService; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -73,6 +74,8 @@ public class HandleServiceImpl implements HandleService { @Autowired protected ClarinItemService clarinItemService; + private EpicHandleService epicHandleService; + private static final Pattern[] IDENTIFIER_PATTERNS = { Pattern.compile("^hdl:(.*)$"), Pattern.compile("^info:hdl/(.*)$"), @@ -86,6 +89,10 @@ public class HandleServiceImpl implements HandleService { protected HandleServiceImpl() { } + public void setEpicHandleService(EpicHandleService epicHandleService) { + this.epicHandleService = epicHandleService; + } + @Override public String resolveToURL(Context context, String handle) throws SQLException { @@ -419,14 +426,15 @@ protected String createId(Context context, DSpaceObject dso) throws SQLException suffix.append(handleSuffix); String prefix = pidCommunityConfiguration.getPrefix(); try { - handleId = DSpaceApi.handle_HandleManager_createId(log, handleSuffix, prefix, suffix.toString()); + handleId = createEpicHandle(handleSuffix, prefix, suffix.toString()); + String resolvedSuffix = handleId.substring(handleId.indexOf("/") + 1); + // if the handle created successfully register the final handle - DSpaceApi - .handle_HandleManager_registerFinalHandleURL(log, handleId, dso); + registerFinalHandleURL(prefix, resolvedSuffix, dso); } catch (IOException e) { throw new IllegalStateException( "External PID service is not working. Please contact the administrator. " - + "Internal message: [" + e.toString() + "]"); + + "Internal message: [" + e.toString() + "]", e); } return handleId; } else if (pidCommunityConfiguration.isLocal()) { @@ -531,4 +539,91 @@ private Event getClarinSetOwningCollectionEvent(Context context) { } return null; } + + private String createEpicHandle(Long id, String prefix, String suffix) throws IOException { + + /* Modified by PP for use pidconsortium.eu at UFAL/CLARIN */ + + String base_url = configurationService.getProperty("dspace.server.url") + "?dummy=" + id; + + /* OK check whether this url has not received pid earlier */ + //This should usually return null (404) + String handle = null; + try { + // handle = PIDService.findHandle(base_url, prefix); + handle = epicHandleService.searchHandles(prefix, base_url, 1, 1).stream() + .map(EpicHandleService.Handle::getHandle) + .findFirst() + .orElse(null); + } catch (Exception e) { + log.error("Error finding handle: " + e); + } + //if not then log and reuse - this is a dummy url, those should not be seen anywhere + if (handle != null) { + log.warn("Url [" + base_url + "] already has PID(s) (" + handle + ")."); + return handle; + } + /* /OK/ */ + + log.debug("Asking for a new PID using a dummy URL " + base_url); + + /* request a new PID, initially pointing to dspace base_uri+id */ + String pid = null; + try { + if (suffix != null && !suffix.isEmpty() && epicHandleService.supportsCustomHandleCreation()) { + pid = epicHandleService.createNewHandleWithSuffix(prefix, suffix, base_url); + } else { + pid = epicHandleService.createHandle(prefix, null, null, base_url); + } + } catch (Exception e) { + throw new IOException(e); + } + + log.debug("got PID " + pid); + return pid; + } + + /** + * Modify an existing PID to point to the corresponding DSpace handle + * + * @exception SQLException If a database error occurs + */ + public void registerFinalHandleURL(String prefix, String suffix, DSpaceObject dso) throws IOException { + String pid = prefix + "/" + suffix; + + String url = generateItemURLWithHandle(pid, dso); + + /* + * request modification of the PID to point to the correct URL, which + * itself should contain the PID as a substring + */ + log.debug("Asking for changing the PID '" + pid + "' to " + url); + + try { + Map fields = HandlePlugin.extractMetadata(dso); + epicHandleService.updateHandleIfExists(prefix, suffix, url, fields); + } catch (Exception e) { + throw new IOException("Failed to map PID " + pid + " to " + url + + " (" + e.toString() + ")"); + } + } + + /** + * Generate a URL with the handle for the given DSpaceObject. + * The URL consist of the base URL and the handle of the DSpace object. + * E.g. `http://localhost:4000/handle/` + * @param pid the pid we are using (obtained e.g. from EPIC) + * @param dSpaceObject the DSpace object for which the URL is generated + * @return the generated URL + */ + public String generateItemURLWithHandle(String pid, DSpaceObject dSpaceObject) { + String url = configurationService.getProperty("dspace.ui.url"); + if (dSpaceObject == null) { + log.error("DSpaceObject is null, cannot generate URL"); + return url; + } + return url + (url.endsWith("/") ? "" : "/") + "handle/" + pid; + } + + } diff --git a/dspace-api/src/main/java/org/dspace/handle/PIDService.java b/dspace-api/src/main/java/org/dspace/handle/PIDService.java deleted file mode 100644 index 615afbcf12e8..000000000000 --- a/dspace-api/src/main/java/org/dspace/handle/PIDService.java +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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.handle; - -/* Created for LINDAT/CLARIAH-CZ (UFAL) */ -/** - * Service for PID. - * - * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) - */ -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Random; - -import org.dspace.services.ConfigurationService; -import org.dspace.utils.DSpace; - -public class PIDService { - public static final String SERVICE_TYPE_EPIC = "epic"; - - public static final String SERVICE_TYPE_EPIC2 = "epic2"; - - private static AbstractPIDService pidService = null; - - private static ConfigurationService configurationService = new DSpace().getConfigurationService(); - - private PIDService() { - - } - - private static void initialize() throws Exception { - if (Objects.nonNull(pidService)) { - return; - } - String serviceType = getServiceType(); - String pidServiceClass = null; - if (serviceType.equals(PIDService.SERVICE_TYPE_EPIC2)) { - pidServiceClass = "org.dspace.handle.PIDServiceEPICv2"; - } else { - throw new IllegalArgumentException("Illegal pid.service type"); - } - try { - pidService = (AbstractPIDService)Class.forName(pidServiceClass).getDeclaredConstructor().newInstance(); - } catch (Exception e) { - throw new Exception(e); - } - } - - public static String getServiceType() { - return configurationService.getProperty("lr.pid.service.type", "lr.pid.service.type"); - } - - /** - * - * @param PID - * @return URL assigned to the PID - * @throws Exception - */ - public static String resolvePID(String PID) throws Exception { - initialize(); - return pidService.resolvePID(PID); - } - - public static String modifyPID(String PID, String URL, Map additionalFields) throws Exception { - initialize(); - Map handleFields = new LinkedHashMap(); - handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); - if (null != additionalFields) { - handleFields.putAll(additionalFields); - } - return pidService.modifyPID(PID, handleFields); - } - - public static String createPID(String URL, String prefix) throws Exception { - initialize(); - Map handleFields = new HashMap(); - handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); - return pidService.createPID(handleFields, prefix); - } - - public static String createCustomPID(String URL, String prefix, String suffix) throws Exception { - initialize(); - Map handleFields = new HashMap(); - handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); - return pidService.createCustomPID(handleFields, prefix, suffix); - } - - public static String findHandle(String URL, String prefix) throws Exception { - initialize(); - Map handleFields = new HashMap(); - handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); - return pidService.findHandle(handleFields, prefix); - } - - public static boolean supportsCustomPIDs() throws Exception { - initialize(); - return pidService.supportsCustomPIDs(); - } - - public static String who_am_i(String encoding) throws Exception { - initialize(); - return pidService.whoAmI(encoding); - } - - public static String deletePID(String PID) throws Exception { - initialize(); - return pidService.deletePID(PID); - } - - public static String test_pid(String PID) throws Exception { - who_am_i(null); - // 1. search for pid - // 2. modify it - resolvePID(PID); - Random randomGenerator = new Random(); - int randomInt = randomGenerator.nextInt(10000); - String url = String.format("http://only.testing.mff.cuni.cz/%d", randomInt); - modifyPID(PID, url, null); - String resolved = resolvePID(PID); - if ( resolved.equals(url) ) { - return "testing succesful"; - } else { - return "testing seemed ok but resolving did not return the expected result"; - } - } -} diff --git a/dspace-api/src/main/java/org/dspace/handle/PIDServiceEPICv2.java b/dspace-api/src/main/java/org/dspace/handle/PIDServiceEPICv2.java deleted file mode 100644 index edc34f2f1720..000000000000 --- a/dspace-api/src/main/java/org/dspace/handle/PIDServiceEPICv2.java +++ /dev/null @@ -1,409 +0,0 @@ -/** - * 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.handle; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Type; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; -import org.apache.commons.lang.StringUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.handle.service.HandleClarinService; - -/* Created for LINDAT/CLARIAH-CZ (UFAL) */ -/** - * Service for PID EPICv2. - * - * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) - */ -public class PIDServiceEPICv2 extends AbstractPIDService { - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(PIDServiceEPICv2.class); - private static final Type handleListType = new TypeToken>() {}.getType(); - - private HandleClarinService handleClarinService = ContentServiceFactory.getInstance().getHandleClarinService(); - - public PIDServiceEPICv2() throws Exception { - super(); - } - - @Override - public String sendPIDCommand(HTTPMethod method, Map params) - throws Exception { - String PID = (String) params.get(PARAMS.PID.toString()); - String data = (String) params.get(PARAMS.DATA.toString()); - String prefix = null; - - if (Objects.isNull(PID)) { - PID = ""; - } else { - prefix = PID.startsWith("/") ? PID.split("/", 3)[1] : PID.split("/", 2)[0]; - } - if (!PID.startsWith("/") && !PIDServiceURL.endsWith("/")) { - PID = "/" + PID; - } - - URL url = new URL(PIDServiceURL + PID); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setDoOutput(true); - conn.setRequestMethod(method.toString()); - conn.setRequestProperty("Content-Type", "application/json"); - conn.setRequestProperty("Accept", "application/json"); - - Map headers = (Map) params - .get(PARAMS.HEADER.toString()); - if (Objects.nonNull(headers)) { - for (Map.Entry header : headers.entrySet()) { - conn.setRequestProperty(header.getKey(), header.getValue()); - } - } - - if (Objects.nonNull(data)) { - OutputStream out = conn.getOutputStream(); - out.write(data.getBytes()); - out.flush(); - } - - int responseCode = conn.getResponseCode(); - - if (responseCode < 200 && responseCode > 206) { - log.error("Failed : HTTP error code : " + responseCode + " : " - + conn.getResponseMessage()); - throw new RuntimeException("Failed : HTTP error code : " - + responseCode + " : " + conn.getResponseMessage()); - } else { - log.debug(responseCode + " : " + conn.getResponseMessage()); - } - - StringBuffer response = new StringBuffer(); - if (responseCode == 201) { - String location = conn.getHeaderField("Location"); - int index; - if (Objects.nonNull(prefix)) { - index = location.indexOf(prefix); - } else { - index = PIDServiceURL.endsWith("/") ? PIDServiceURL.length() - : (PIDServiceURL.length() + 1); - } - response.append(location.substring(index)); - } else { - BufferedReader br = new BufferedReader(new InputStreamReader( - (conn.getInputStream()))); - String line = null; - while ((line = br.readLine()) != null) { - response.append(line).append("\n"); - } - } - conn.disconnect(); - return response.toString(); - } - - /** - * Returns URL - */ - @Override - public String resolvePID(String PID) throws Exception { - HashMap params = new HashMap(); - params.put(PARAMS.PID.toString(), PID); - String response = sendPIDCommand(HTTPMethod.GET, params); - Gson gson = getGsonWithHandleDeserializers(null); - Handle handle = gson.fromJson(response, Handle.class); - return handle.getUrl(); - } - - @Override - public String createPID(Map handleFields, String prefix) - throws Exception { - JsonArray data = getEPICJsonRepresentation(handleFields); - HashMap params = new HashMap(); - params.put(PARAMS.PID.toString(), prefix); - params.put(PARAMS.DATA.toString(), data.toString()); - return sendPIDCommand(HTTPMethod.POST, params); - } - - @Override - public String createCustomPID(Map handleFields, - String prefix, String suffix) throws Exception { - JsonArray data = getEPICJsonRepresentation(handleFields); - HashMap params = new HashMap(); - params.put(PARAMS.PID.toString(), handleClarinService.completeHandle(prefix, suffix)); - params.put(PARAMS.DATA.toString(), data.toString()); - - HashMap headers = new HashMap(); - headers.put("If-None-Match", "*"); - - params.put(PARAMS.HEADER.toString(), headers); - - return sendPIDCommand(HTTPMethod.PUT, params); - } - - @Override - public String modifyPID(String PID, Map handleFields) - throws Exception { - JsonArray data = getEPICJsonRepresentation(handleFields); - HashMap params = new HashMap(); - params.put(PARAMS.PID.toString(), PID); - params.put(PARAMS.DATA.toString(), data.toString()); - - HashMap headers = new HashMap(); - headers.put("If-Match", "*"); - - params.put(PARAMS.HEADER.toString(), headers); - - return sendPIDCommand(HTTPMethod.PUT, params); - } - - @Override - public String deletePID(String PID) throws Exception { - HashMap params = new HashMap(); - params.put(PARAMS.PID.toString(), PID); - return sendPIDCommand(HTTPMethod.DELETE, params); - } - - @Override - public String findHandle(Map handleFields, String prefix) - throws Exception { - HashMap params = new HashMap(); - params.put(PARAMS.PID.toString(), prefix + "/?" - + getQueryString(handleFields)); - String response = sendPIDCommand(HTTPMethod.GET, params); - String[] pids = new Gson().fromJson(response, String[].class); - if (pids.length == 0) { - return null; - } - return StringUtils.join(pids, ","); - } - - public List findHandles(Map handleFields, String prefix, - String depth, int limit, int page) throws Exception { - HashMap params = new HashMap(); - HashMap headers = new HashMap<>(); - addDepth(headers, depth); - if (!headers.isEmpty()) { - params.put(PARAMS.HEADER.toString(), headers); - } - addLimitPage(handleFields, limit, page); - params.put(PARAMS.PID.toString(), prefix + "/?" - + getQueryString(handleFields)); - String response = sendPIDCommand(HTTPMethod.GET, params); - Gson gson = getGsonWithHandleDeserializers(prefix); - return gson.fromJson(response, handleListType); - } - - public List findHandles(String query, String prefix, String depth, int limit, int page) throws Exception { - Map handleFields = new HashMap<>(); - //surround the query with **, allows substring matches - handleFields.put(HANDLE_FIELDS.URL.toString(), String.format("*%s*",query)); - return findHandles(handleFields, prefix, depth, limit, page); - } - - @Override - public boolean supportsCustomPIDs() { - return true; - } - - @Override - public String whoAmI(String encoding) throws Exception { - return "There is no implementation of whoAmI in v2 you are logging in as " - + PIDServiceUSER; - } - - public List list(String prefix, String depth, int limit, int page) - throws Exception { - HashMap params = new HashMap<>(); - - HashMap headers = new HashMap<>(); - addDepth(headers, depth); - if (!headers.isEmpty()) { - params.put(PARAMS.HEADER.toString(), headers); - } - - HashMap fields = new HashMap<>(); - addLimitPage(fields, limit, page); - params.put(PARAMS.PID.toString(), prefix + "/?" - + getQueryString(fields)); - String response = sendPIDCommand(HTTPMethod.GET, params); - - Gson gson = getGsonWithHandleDeserializers(prefix); - return gson.fromJson(response, handleListType); - } - - public List listAllHandles(String prefix) throws Exception { - return list(prefix, "1", 0, 0); - } - - public int getCount(String prefix) throws Exception { - return list(prefix,"0",0,0).size(); - } - - public int getResultCount(String prefix, String query) throws Exception { - return findHandles(query, prefix, "0", 0, 0).size(); - } - - private String getQueryString(Map handleFields) { - StringBuffer qstr = new StringBuffer(); - for (Map.Entry entry : handleFields.entrySet()) { - qstr.append("&"); - qstr.append(entry.getKey()); - qstr.append("="); - qstr.append(entry.getValue()); - } - return qstr.substring(1); - } - - private void addDepth(Map headers, String depth) { - if (depth != null && depth.matches("^(0|1|infinity)$")) { - headers.put("Depth", depth); - } - } - - private void addLimitPage(Map fields, int limit, int page) { - fields.put("limit", Integer.toString(limit)); - if (limit > 0) { - fields.put("page", Integer.toString(page)); - } - } - - private Gson getGsonWithHandleDeserializers(String prefix) { - // Configure Gson - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(Handle.class, new HandleDeserializer()); - if (prefix != null) { - gsonBuilder.registerTypeAdapter(handleListType, new HandlesDeserializer( - prefix)); - } - return gsonBuilder.create(); - } - - private JsonArray getEPICJsonRepresentation(Map handleFields) { - JsonArray json_rep = new JsonArray(); - for (Map.Entry entry : handleFields.entrySet()) { - JsonObject json_obj = new JsonObject(); - json_obj.addProperty("type", entry.getKey()); - json_obj.addProperty("parsed_data", entry.getValue()); - json_rep.add(json_obj); - } - return json_rep; - } - - public static class Handle { - - private String handle; - - private String url; - - public Handle(String handle, String url) { - this.handle = handle; - this.url = url; - } - - public Handle(String handle) { - this(handle, null); - } - - public Handle() { - this(null, null); - } - - public String getHandle() { - return handle; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public void setHandle(String handle) { - this.handle = handle; - } - - } - - static class HandleDeserializer implements JsonDeserializer { - - @Override - public Handle deserialize(JsonElement json, Type typeOfT, - JsonDeserializationContext context) throws JsonParseException { - JsonArray jsonInfo = json.getAsJsonArray(); - for (JsonElement el : jsonInfo) { - JsonObject obj = el.getAsJsonObject(); - JsonElement jsonType = obj.get("type"); - if (jsonType != null) { - String type = jsonType.getAsString(); - if (type.equals("URL")) { - String url = obj.get("parsed_data").getAsString(); - Handle h = new Handle(); - h.setUrl(url); - return h; - } - } - } - throw new JsonParseException("Failed to find URL for this handle.\n" + json.toString()); - } - } - - static class HandlesDeserializer implements JsonDeserializer> { - - private final String prefix; - - public HandlesDeserializer(String prefix) { - this.prefix = prefix; - } - - @Override - public List deserialize(JsonElement json, Type typeOfT, - JsonDeserializationContext context) throws JsonParseException { - ArrayList handles = new ArrayList<>(); - if (json.isJsonArray()) { - String[] ids = context.deserialize(json, String[].class); - for (String id : ids) { - handles.add(new Handle(prefix + "/" + id)); - } - } else { - JsonObject jsonObject = json.getAsJsonObject(); - for (Map.Entry entry : jsonObject.entrySet()) { - // remove /handles/ to match ids provided with Depth: 0 - String id = entry.getKey().replaceFirst("/handles/", ""); - try { - Handle h = context.deserialize(entry.getValue(), Handle.class); - h.setHandle(id); - handles.add(h); - } catch (JsonParseException e) { - //there are handles with no url - Handle h = new Handle(); - h.setHandle(id); - handles.add(h); - //throw new JsonParseException("Failed to parse " + id, e); - } - } - } - return handles; - } - } -} diff --git a/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java b/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java index 76a4a5c6da08..8a5ddd75458f 100644 --- a/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java +++ b/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java @@ -9,8 +9,11 @@ import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.Objects; +import org.dspace.handle.EpicHandleField; + /** * Interface to help with * ePIC handles REST API. @@ -19,6 +22,8 @@ */ public interface EpicHandleService { + public static final String EPIC_HANDLE_SERVICE_URL = "epic.handle.service.url"; + /** * Returns the URL for handle, or null if handle cannot be found. * @@ -42,6 +47,26 @@ public interface EpicHandleService { */ String createHandle(String prefix, String subPrefix, String subSuffix, String url) throws IOException; + /** + * Creates new handle with given prefix/suffix. Returns the handle created or throws Exception. + * + * @param prefix The handle prefix + * @param suffix The handle suffix + * @param url url associated with the handle (required) + * @return The full handle String (prefix/suffix) + * @throws IOException If request to ePIC handle server fails + */ + String createNewHandleWithSuffix(String prefix, String suffix, String url) throws IOException; + + /** + * Returns true if the handle service supports creating handles with custom suffix (suffix defined by client) + * + * @return true if the handle service supports creating handles with custom suffix, false otherwise + */ + default boolean supportsCustomHandleCreation() { + return true; + } + /** * Creates new handle with given prefix/suffix or updates handle when this handle already exists. * Returns the handle when handle is created or null when handle is updated, or throws Exception. @@ -55,6 +80,18 @@ public interface EpicHandleService { */ String createOrUpdateHandle(String prefix, String suffix, String url) throws IOException; + /** + * Updates handle with given prefix/suffix. + * + * @param prefix The handle prefix + * @param suffix The handle suffix + * @param url url associated with the handle (required) + * @param data map with additional fields to update, keys are defined in EpicHandleFields enum + * @throws IOException If request to ePIC handle server fails + */ + void updateHandleIfExists(String prefix, String suffix, String url, Map data) + throws IOException; + /** * Returns no content or throws Exception * diff --git a/dspace-api/src/test/java/org/dspace/handle/PIDConfigurationTest.java b/dspace-api/src/test/java/org/dspace/handle/PIDConfigurationTest.java index a655f59853a3..2d645057c981 100644 --- a/dspace-api/src/test/java/org/dspace/handle/PIDConfigurationTest.java +++ b/dspace-api/src/test/java/org/dspace/handle/PIDConfigurationTest.java @@ -16,7 +16,6 @@ import java.util.UUID; import org.dspace.AbstractUnitTest; -import org.dspace.api.DSpaceApi; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -27,8 +26,6 @@ import org.dspace.content.service.CommunityService; import org.dspace.content.service.InstallItemService; import org.dspace.content.service.WorkspaceItemService; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -129,6 +126,7 @@ public void testInitCommunityConfigMapShouldNotBeShared() throws NoSuchFieldExce assertEquals("Com2 should still have local type", "local", pidCommunityConfiguration2.getType()); } + /* @Test public void testGeneratingItemURL() { ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -157,4 +155,5 @@ public void testGeneratingItemURL() { url = DSpaceApi.generateItemURLWithHandle(pid, publicItem); assertEquals(expectedUrl, url); } + */ } diff --git a/dspace/config/clarin-dspace.cfg b/dspace/config/clarin-dspace.cfg index a481beb856aa..3a5ffd937e36 100644 --- a/dspace/config/clarin-dspace.cfg +++ b/dspace/config/clarin-dspace.cfg @@ -25,8 +25,8 @@ spring.servlet.multipart.max-request-size = 4GB #------------------------------------------------------------------# # PID service -# type o service; for now only epic and epic2 are supported -lr.pid.service.type = epic2 +# type o service; for now only epic2 is supported +epic.handle.service.implementation = epic2 lr.pid.service.url = https://handle.gwdg.de/pidservice/ lr.pid.service.user = lr.pid.service.pass = diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 9017ea7fae75..b432315d5833 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -127,9 +127,12 @@ - + + + + - + From 5168ce9bf21aa7b5fe459e76fd911935347c30e9 Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Tue, 5 May 2026 10:43:48 +0200 Subject: [PATCH 2/4] update ePIC Handle metadata values when item is installed --- .../content/InstallItemServiceImpl.java | 9 ++++- .../dspace/handle/EpicHandleServiceImpl.java | 2 +- .../org/dspace/handle/HandleServiceImpl.java | 40 ++++++++++++++++++- .../handle/service/EpicHandleService.java | 2 +- .../dspace/handle/service/HandleService.java | 15 ++++++- 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java index 12047d8654a1..16a1fe1da8c6 100644 --- a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java @@ -35,6 +35,7 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.service.GroupService; import org.dspace.event.Event; +import org.dspace.handle.service.HandleService; import org.dspace.identifier.Identifier; import org.dspace.identifier.IdentifierException; import org.dspace.identifier.service.IdentifierService; @@ -78,6 +79,8 @@ public class InstallItemServiceImpl implements InstallItemService { protected VersioningService versioningService; @Autowired(required = true) protected VersionHistoryService versionHistoryService; + @Autowired(required = true) + protected HandleService handleService; Logger log = LogManager.getLogger(InstallItemServiceImpl.class); @@ -115,7 +118,11 @@ public Item installItem(Context c, InProgressSubmission is, // Finish up / archive the item item = finishItem(c, item, is); - fixRelationMetadata(c, item); + try { + handleService.updateHandleMetadata(c, item); + } catch (IOException e) { + log.error(String.format("Couldn't update handle metadata for item with id %s", item.getID().toString()), e); + } // As this is a BRAND NEW item, as a final step we need to remove the // submitter item policies created during deposit and replace them with diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java index 377caf65be9b..6b7f0a67c9f2 100644 --- a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java @@ -155,7 +155,7 @@ public String createOrUpdateHandle(String prefix, String suffix, String url) thr } @Override - public void updateHandleIfExists(String prefix, String suffix, String url, Map data) + public void updateExistingHandle(String prefix, String suffix, String url, Map data) throws IOException { initialize(); String jsonData = getJsonData(objectMapper, url, data).toString(); diff --git a/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java index 8f140077da16..e1f61e7a5f17 100644 --- a/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/HandleServiceImpl.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -498,6 +499,42 @@ public String[] getAdditionalPrefixes() { return configurationService.getArrayProperty("handle.additional.prefixes"); } + @Override + public void updateHandleMetadata(Context context, DSpaceObject dso) throws SQLException, IOException { + if (!(dso instanceof Item)) { + //update handle metadata for another type of dspace objects is not supported + return; + } + + Item item = (Item) dso; + Collection collection = item.getOwningCollection(); + + List communities = collection.getCommunities(); + if (communities != null && !communities.isEmpty()) { + PIDCommunityConfiguration pidCommunityConfiguration = PIDConfiguration + .getPIDCommunityConfiguration(communities.get(0).getID()); + if (pidCommunityConfiguration != null && pidCommunityConfiguration.isEpic()) { + String prefix = pidCommunityConfiguration.getPrefix(); + String handle = item.getHandles().stream() + .map(Handle::getHandle) + .filter(h -> h.startsWith(prefix + "/")) + .findFirst() + .orElse(null); + if (handle != null) { + String suffix = handle.substring(handle.indexOf("/") + 1); + String url = epicHandleService.resolveURLForHandle(prefix, suffix); + if (url != null) { + Map fields = HandlePlugin.extractMetadata(dso); + epicHandleService.updateExistingHandle(prefix, suffix, url, fields); + } else { + log.warn("Cannot update handle metadata for ePIC handle {}", prefix + "/" + suffix); + } + } + } + } + + } + /** * * @param context DSpace context @@ -550,7 +587,6 @@ private String createEpicHandle(Long id, String prefix, String suffix) throws IO //This should usually return null (404) String handle = null; try { - // handle = PIDService.findHandle(base_url, prefix); handle = epicHandleService.searchHandles(prefix, base_url, 1, 1).stream() .map(EpicHandleService.Handle::getHandle) .findFirst() @@ -601,7 +637,7 @@ public void registerFinalHandleURL(String prefix, String suffix, DSpaceObject ds try { Map fields = HandlePlugin.extractMetadata(dso); - epicHandleService.updateHandleIfExists(prefix, suffix, url, fields); + epicHandleService.updateExistingHandle(prefix, suffix, url, fields); } catch (Exception e) { throw new IOException("Failed to map PID " + pid + " to " + url + " (" + e.toString() + ")"); diff --git a/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java b/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java index 8a5ddd75458f..d6b60143d559 100644 --- a/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java +++ b/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java @@ -89,7 +89,7 @@ default boolean supportsCustomHandleCreation() { * @param data map with additional fields to update, keys are defined in EpicHandleFields enum * @throws IOException If request to ePIC handle server fails */ - void updateHandleIfExists(String prefix, String suffix, String url, Map data) + void updateExistingHandle(String prefix, String suffix, String url, Map data) throws IOException; /** diff --git a/dspace-api/src/main/java/org/dspace/handle/service/HandleService.java b/dspace-api/src/main/java/org/dspace/handle/service/HandleService.java index 85950ab6db87..48a06e9a9f98 100644 --- a/dspace-api/src/main/java/org/dspace/handle/service/HandleService.java +++ b/dspace-api/src/main/java/org/dspace/handle/service/HandleService.java @@ -7,6 +7,7 @@ */ package org.dspace.handle.service; +import java.io.IOException; import java.sql.SQLException; import java.util.List; @@ -180,6 +181,18 @@ public List getHandlesForPrefix(Context context, String prefix) public void modifyHandleDSpaceObject(Context context, String handle, DSpaceObject newOwner) throws SQLException; + /** + * Update the metadata of the handle record for the given DSpaceObject. + * This is used to update the handle metadata values stored in the handle record, such as the title or the URL. + * + * @param context DSpace context + * @param dso The DSpaceObject whose handle metadata should be updated + * @throws SQLException If a database error occurs + * @throws IOException If an error occurs while updating the handle metadata, + * e.g. when communicating with an external handle service + */ + public void updateHandleMetadata(Context context, DSpaceObject dso) throws SQLException, IOException; + int countTotal(Context context) throws SQLException; /** @@ -196,7 +209,7 @@ public List getHandlesForPrefix(Context context, String prefix) /** * Gets the additional prefixes used for handles, * mapped in configuration file. - * + * * @return `String[]` array of prefixes */ String[] getAdditionalPrefixes(); From ba9f8a680456a93e38be4d233b26c9ff5512aa6b Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Tue, 5 May 2026 11:30:01 +0200 Subject: [PATCH 3/4] fixed failing test, resolve type --- .../main/java/org/dspace/content/InstallItemServiceImpl.java | 1 + .../src/main/java/org/dspace/handle/EpicHandleRestHelper.java | 2 +- .../src/main/java/org/dspace/handle/EpicHandleServiceImpl.java | 2 +- dspace/config/clarin-dspace.cfg | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java index 16a1fe1da8c6..645803e92934 100644 --- a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java @@ -118,6 +118,7 @@ public Item installItem(Context c, InProgressSubmission is, // Finish up / archive the item item = finishItem(c, item, is); + fixRelationMetadata(c, item); try { handleService.updateHandleMetadata(c, item); } catch (IOException e) { diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java index 947bb6f160c2..32aa3895ff07 100644 --- a/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java @@ -68,7 +68,7 @@ public static Response updateHandle(String pidServiceURL, String prefix, String return putHandle(pidServiceURL, prefix, suffix, jsonData, null); } - public static Response updateHandleIfExists(String pidServiceURL, String prefix, String suffix, String jsonData) { + public static Response updateExistingHandle(String pidServiceURL, String prefix, String suffix, String jsonData) { return putHandle(pidServiceURL, prefix, suffix, jsonData, Map.of("If-Match", "*")); } diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java index 6b7f0a67c9f2..892fe2ea6d45 100644 --- a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java @@ -159,7 +159,7 @@ public void updateExistingHandle(String prefix, String suffix, String url, Map Date: Tue, 5 May 2026 14:31:25 +0200 Subject: [PATCH 4/4] resolve Copilot comment --- .../main/java/org/dspace/handle/EpicHandleServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java index 892fe2ea6d45..a22f12c45be6 100644 --- a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java @@ -128,11 +128,13 @@ public String createNewHandleWithSuffix(String prefix, String suffix, String url initialize(); String jsonData = getJsonDataForUrl(objectMapper, url).toString(); + // Either creates a new handle with the given prefix and suffix, + // or fails if the handle with given prefix and suffix already exists (412 Precondition Failed error). try (Response response = EpicHandleRestHelper.createNewHandleWithSuffix(pidServiceUrl, prefix, suffix, jsonData)) { if (response.getStatus() == Response.Status.CREATED.getStatusCode()) { return objectMapper.readValue(response.readEntity(String.class), EpicPid.class).getHandle(); - } else { + } else { throw new WebApplicationException(response); } }