diff --git a/docs/manual/docs/user-guide/associating-resources/doi.md b/docs/manual/docs/user-guide/associating-resources/doi.md index 314348316884..859835326603 100644 --- a/docs/manual/docs/user-guide/associating-resources/doi.md +++ b/docs/manual/docs/user-guide/associating-resources/doi.md @@ -7,9 +7,21 @@ The catalogue support DOI creation using: - [DataCite API](https://support.datacite.org/docs/mds-api-guide). - EU publication office API -Configure the API access point in the `admin console --> settings`: +Configure the DOI API access point to publish the metadata in the `Admin console --> Settings --> Doi servers`: -![](img/doi-admin-console.png) +![](img/doi-create-server.png) + +Providing the following information: + +- `Name`: A descriptive name for the server. +- `Description`: (Optional) A verbose description of the server. +- `DataCite API endpoint`: The API url, usually https://mds.datacite.org or https://mds.test.datacite.org for testing. +- `DataCite username` / `DataCite password`: Credentials required to publish the DOI resources. +- `Landing page URL template`: The URL to use to register the DOI. A good default for GeoNetwork is http://localhost:8080/geonetwork/srv/resources/records/{{uuid}}. The landing page URL MUST contains the UUID of the record. +- `Final DOI URL prefix`: (Optional) Keep it empty to use the default https://doi.org prefix. Use https://mds.test.datacite.org/doi when using the test API. +- `DOI pattern`: Default is `{{uuid}}` but the DOI structure can be customized with database id and/or record group eg. `example-{{groupOwner}}-{{id}}`. +- `DataCite prefix`: Usually looks like `10.xxxx`. You will be allowed to register DOI names only under the prefixes that have been assigned to you. +- `Publication groups`: (Optional) Select the groups which metadata should be published to the DOI server. If no groups are selected, the server will be provided to publish the metadata that has no other DOI servers related to the metadata owner group. A record can be downloaded using the DataCite format from the API using: diff --git a/docs/manual/docs/user-guide/associating-resources/img/doi-create-server.png b/docs/manual/docs/user-guide/associating-resources/img/doi-create-server.png new file mode 100644 index 000000000000..efccf6030657 Binary files /dev/null and b/docs/manual/docs/user-guide/associating-resources/img/doi-create-server.png differ diff --git a/doi/src/main/java/org/fao/geonet/doi/client/DoiManager.java b/doi/src/main/java/org/fao/geonet/doi/client/DoiManager.java index 87871c21d72c..3c8204ea6c80 100644 --- a/doi/src/main/java/org/fao/geonet/doi/client/DoiManager.java +++ b/doi/src/main/java/org/fao/geonet/doi/client/DoiManager.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2010 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -32,8 +32,8 @@ import org.fao.geonet.domain.*; import org.fao.geonet.kernel.AccessManager; import org.fao.geonet.kernel.ApplicableSchematron; -import org.fao.geonet.kernel.DataManager; import org.fao.geonet.kernel.SchematronValidator; +import org.fao.geonet.kernel.datamanager.base.BaseMetadataManager; import org.fao.geonet.kernel.datamanager.base.BaseMetadataSchemaUtils; import org.fao.geonet.kernel.datamanager.base.BaseMetadataUtils; import org.fao.geonet.kernel.schema.MetadataSchema; @@ -41,12 +41,10 @@ import org.fao.geonet.kernel.search.IndexingMode; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.repository.SchematronRepository; -import org.fao.geonet.utils.Log; import org.fao.geonet.utils.Xml; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; -import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; import java.nio.file.Files; @@ -60,11 +58,9 @@ /** * Class to register/unregister DOIs using the Datacite Metadata Store (MDS) API. + *

+ * See ... * - * See https://support.datacite.org/docs/mds-api-guide - * - * @author Jose García - * @author Francois Prunayre */ public class DoiManager { private static final String DOI_ADD_XSL_PROCESS = "process/doi-add.xsl"; @@ -75,112 +71,52 @@ public class DoiManager { public static final String DOI_DEFAULT_URL = "https://doi.org/"; public static final String DOI_DEFAULT_PATTERN = "{{uuid}}"; - private IDoiClient client; - private String doiPrefix; - private String doiPattern; - private String landingPageTemplate; - private boolean initialised = false; - private boolean isMedra = false; - - DataManager dm; - SettingManager sm; - BaseMetadataSchemaUtils schemaUtils; - - @Autowired - BaseMetadataUtils metadataUtils; - - @Autowired - SchematronValidator validator; - - @Autowired - DoiBuilder doiBuilder; + private final SettingManager sm; + private final BaseMetadataSchemaUtils schemaUtils; + private final BaseMetadataManager metadataManager; + private final BaseMetadataUtils metadataUtils; + private final SchematronValidator validator; + private final DoiBuilder doiBuilder; + private final SchematronRepository schematronRepository; + + + public DoiManager(final SettingManager sm, final BaseMetadataSchemaUtils schemaUtils, + final BaseMetadataManager metadataManager, final BaseMetadataUtils metadataUtils, + final SchematronValidator validator, final DoiBuilder doiBuilder, + final SchematronRepository schematronRepository) { + this.sm = sm; + this.schemaUtils = schemaUtils; + this.metadataManager = metadataManager; + this.metadataUtils = metadataUtils; + this.validator = validator; + this.doiBuilder = doiBuilder; + this.schematronRepository = schematronRepository; - @Autowired - SchematronRepository schematronRepository; - - - public DoiManager() { - sm = ApplicationContextHolder.get().getBean(SettingManager.class); - dm = ApplicationContextHolder.get().getBean(DataManager.class); - schemaUtils = ApplicationContextHolder.get().getBean(BaseMetadataSchemaUtils.class); - - loadConfig(); } - public boolean isInitialised() { - return initialised; + private IDoiClient createDoiClient(DoiServer doiServer) { + boolean isMedra = isMedraServer(doiServer); + return isMedra ? + new DoiMedraClient(doiServer.getUrl(), doiServer.getUsername(), doiServer.getPassword(), doiServer.getPublicUrl()) : + new DoiDataciteClient(doiServer.getUrl(), doiServer.getUsername(), doiServer.getPassword(), doiServer.getPublicUrl()); } - /** - * Check parameters and build the client. - * - */ - public void loadConfig() { - initialised = false; - if (sm != null) { - - String serverUrl = sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_DOIURL); - String doiPublicUrl = StringUtils.defaultIfEmpty( - sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_DOIPUBLICURL), - DOI_DEFAULT_URL); - String username = sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_DOIUSERNAME); - String password = sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_DOIPASSWORD); - - doiPrefix = sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_DOIKEY); - doiPattern = StringUtils.defaultIfEmpty( - sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_DOIPATTERN), - DOI_DEFAULT_PATTERN - ); - - landingPageTemplate = sm.getValue(DoiSettings.SETTING_PUBLICATION_DOI_LANDING_PAGE_TEMPLATE); - - final boolean emptyUrl = StringUtils.isEmpty(serverUrl); - final boolean emptyUsername = StringUtils.isEmpty(username); - final boolean emptyPassword = StringUtils.isEmpty(password); - final boolean emptyPrefix = StringUtils.isEmpty(doiPrefix); - if (emptyUrl || - emptyUsername || - emptyPassword || - emptyPrefix) { - StringBuilder report = new StringBuilder("DOI configuration is not complete. Check in System Configuration to fill the DOI configuration."); - if (emptyUrl) { - report.append("\n* URL MUST be set"); - } - if (emptyUsername) { - report.append("\n* Username MUST be set"); - } - if (emptyPassword) { - report.append("\n* Password MUST be set"); - } - if (emptyPrefix) { - report.append("\n* Prefix MUST be set"); - } - Log.warning(DoiSettings.LOGGER_NAME, - report.toString()); - } else { - Log.debug(DoiSettings.LOGGER_NAME, - "DOI configuration looks perfect."); - isMedra = serverUrl.contains(MEDRA_SEARCH_KEY); - this.client = - isMedra ? - new DoiMedraClient(serverUrl, username, password, doiPublicUrl) : - new DoiDataciteClient(serverUrl, username, password, doiPublicUrl); - initialised = true; - } - } - } + public String checkDoiUrl(DoiServer doiServer, AbstractMetadata metadata) throws DoiClientException { + checkInitialised(doiServer); + checkCanHandleMetadata(doiServer, metadata); - public String checkDoiUrl(AbstractMetadata metadata) { - return doiBuilder.create(doiPattern, doiPrefix, metadata); + return doiBuilder.create(doiServer.getPattern(), doiServer.getPrefix(), metadata); } - public Map check(ServiceContext serviceContext, AbstractMetadata metadata, Element dataciteMetadata) throws Exception { + public Map check(ServiceContext serviceContext, DoiServer doiServer, AbstractMetadata metadata, Element dataciteMetadata) throws Exception { Map conditions = new HashMap<>(); - checkInitialised(); + checkInitialised(doiServer); + checkCanHandleMetadata(doiServer, metadata); conditions.put(DoiConditions.API_CONFIGURED, true); - String doi = doiBuilder.create(doiPattern, doiPrefix, metadata); - checkPreConditions(metadata, doi); + IDoiClient doiClient = createDoiClient(doiServer); + String doi = doiBuilder.create(doiServer.getPattern(), doiServer.getPrefix(), metadata); + checkPreConditions(doiClient, metadata, doi); conditions.put(DoiConditions.RECORD_IS_PUBLIC, true); conditions.put(DoiConditions.STANDARD_SUPPORT, true); @@ -188,26 +124,26 @@ public Map check(ServiceContext serviceContext, AbstractMetadat // ** Convert to DataCite format Element dataciteFormatMetadata = dataciteMetadata == null ? - convertXmlToDataCiteFormat(metadata.getDataInfo().getSchemaId(), - metadata.getXmlData(false), doi) : dataciteMetadata; - checkPreConditionsOnDataCite(metadata, doi, dataciteFormatMetadata, serviceContext.getLanguage()); + convertXmlToDataCiteFormat(doiServer, metadata.getDataInfo().getSchemaId(), + metadata.getXmlData(false), doi) : dataciteMetadata; + checkPreConditionsOnDataCite(doiClient, metadata, doi, dataciteFormatMetadata, serviceContext.getLanguage()); conditions.put(DoiConditions.DATACITE_FORMAT_IS_VALID, true); return conditions; } - public Map register(ServiceContext context, AbstractMetadata metadata) throws Exception { + public Map register(ServiceContext context, DoiServer doiServer, AbstractMetadata metadata) throws Exception { Map doiInfo = new HashMap<>(3); // The new DOI for this record - String doi = doiBuilder.create(doiPattern, doiPrefix, metadata); + String doi = doiBuilder.create(doiServer.getPattern(), doiServer.getPrefix(), metadata); doiInfo.put("doi", doi); // The record in datacite format Element dataciteFormatMetadata = - convertXmlToDataCiteFormat(metadata.getDataInfo().getSchemaId(), - metadata.getXmlData(false), doi); + convertXmlToDataCiteFormat(doiServer, metadata.getDataInfo().getSchemaId(), + metadata.getXmlData(false), doi); try { - check(context, metadata, dataciteFormatMetadata); + check(context, doiServer, metadata, dataciteFormatMetadata); } catch (ResourceAlreadyExistException ignore) { // Update DOI doiInfo.put("update", "true"); @@ -215,7 +151,8 @@ public Map register(ServiceContext context, AbstractMetadata met throw e; } - createDoi(context, metadata, doiInfo, dataciteFormatMetadata); + IDoiClient doiClient = createDoiClient(doiServer); + createDoi(context, doiClient, doiServer, metadata, doiInfo, dataciteFormatMetadata); checkDoiCreation(metadata, doiInfo); return doiInfo; @@ -230,7 +167,7 @@ public Map register(ServiceContext context, AbstractMetadata met * @throws IOException * @throws JDOMException */ - private void checkPreConditions(AbstractMetadata metadata, String doi) throws DoiClientException, IOException, JDOMException, ResourceAlreadyExistException { + private void checkPreConditions(IDoiClient doiClient, AbstractMetadata metadata, String doi) throws DoiClientException, IOException, JDOMException, ResourceAlreadyExistException { // Record MUST be public AccessManager am = ApplicationContextHolder.get().getBean(AccessManager.class); boolean visibleToAll = false; @@ -239,11 +176,11 @@ private void checkPreConditions(AbstractMetadata metadata, String doi) throws Do } catch (Exception e) { throw new DoiClientException(String.format( "Failed to check if record '%s' is visible to all for DOI creation." + - " Error is %s.", + " Error is %s.", metadata.getUuid(), e.getMessage())) .withMessageKey("exception.doi.failedVisibilityCheck") .withDescriptionKey("exception.doi.failedVisibilityCheck.description", - new String[]{ metadata.getUuid(), e.getMessage() }); + new String[]{metadata.getUuid(), e.getMessage()}); } if (!visibleToAll) { @@ -251,7 +188,7 @@ private void checkPreConditions(AbstractMetadata metadata, String doi) throws Do "Record '%s' is not public and we cannot request a DOI for such a record. Publish this record first.", metadata.getUuid())) .withMessageKey("exception.doi.recordNotPublic") - .withDescriptionKey("exception.doi.recordNotPublic.description", new String[]{ metadata.getUuid() }); + .withDescriptionKey("exception.doi.recordNotPublic.description", new String[]{metadata.getUuid()}); } // Record MUST not contains a DOI @@ -259,7 +196,7 @@ private void checkPreConditions(AbstractMetadata metadata, String doi) throws Do String currentDoi = metadataUtils.getDoi(metadata.getUuid()); if (StringUtils.isNotEmpty(currentDoi)) { // Current doi does not match the one going to be inserted. This is odd - String newDoi = client.createPublicUrl(doi); + String newDoi = doiClient.createPublicUrl(doi); if (!currentDoi.equals(newDoi)) { throw new DoiClientException(String.format( "Record '%s' already contains a DOI %s which is not equal " + @@ -269,7 +206,7 @@ private void checkPreConditions(AbstractMetadata metadata, String doi) throws Do "an existing DOI.", metadata.getUuid(), currentDoi, currentDoi, newDoi)) .withMessageKey("exception.doi.resourcesContainsDoiNotEqual") - .withDescriptionKey("exception.doi.resourcesContainsDoiNotEqual.description", new String[]{ metadata.getUuid(), currentDoi, currentDoi, newDoi }); + .withDescriptionKey("exception.doi.resourcesContainsDoiNotEqual.description", new String[]{metadata.getUuid(), currentDoi, currentDoi, newDoi}); } throw new ResourceAlreadyExistException(String.format( @@ -279,7 +216,7 @@ private void checkPreConditions(AbstractMetadata metadata, String doi) throws Do metadata.getUuid(), currentDoi, currentDoi)) .withMessageKey("exception.doi.resourceContainsDoi") .withDescriptionKey("exception.doi.resourceContainsDoi.description", - new String[]{ metadata.getUuid(), currentDoi, currentDoi }); + new String[]{metadata.getUuid(), currentDoi, currentDoi}); } } catch (ResourceNotFoundException e) { final MetadataSchema schema = schemaUtils.getSchema(metadata.getDataInfo().getSchemaId()); @@ -299,24 +236,23 @@ private void checkPreConditions(AbstractMetadata metadata, String doi) throws Do schema.getName())) .withMessageKey("exception.doi.missingSavedquery") .withDescriptionKey("exception.doi.missingSavedquery.description", - new String[]{ metadata.getUuid(), schema.getName(), - SavedQuery.DOI_GET, e.getMessage(), - schema.getName() }); + new String[]{metadata.getUuid(), schema.getName(), + SavedQuery.DOI_GET, e.getMessage(), + schema.getName()}); } } /** * Check conditions on DataCite side. + * * @param metadata * @param doi * @param dataciteMetadata * @param language */ - private void checkPreConditionsOnDataCite(AbstractMetadata metadata, String doi, Element dataciteMetadata, String language) throws DoiClientException, ResourceAlreadyExistException { + private void checkPreConditionsOnDataCite(IDoiClient doiClient, AbstractMetadata metadata, String doi, Element dataciteMetadata, String language) throws DoiClientException, ResourceAlreadyExistException { // * DataCite API is up an running ? - - try { List validations = new ArrayList<>(); List applicableSchematron = Lists.newArrayList(); @@ -341,7 +277,7 @@ private void checkPreConditionsOnDataCite(AbstractMetadata metadata, String doi, StringBuilder message = new StringBuilder(); if (!failures.isEmpty()) { message.append("

"); throw new DoiClientException(String.format( @@ -349,9 +285,9 @@ private void checkPreConditionsOnDataCite(AbstractMetadata metadata, String doi, metadata.getUuid(), failures.size(), message)) .withMessageKey("exception.doi.recordNotConformantMissingInfo") .withDescriptionKey("exception.doi.recordNotConformantMissingInfo.description", - new String[]{ metadata.getUuid(), String.valueOf(failures.size()), message.toString() }); + new String[]{metadata.getUuid(), String.valueOf(failures.size()), message.toString()}); } - } catch (IOException|JDOMException e) { + } catch (IOException | JDOMException e) { throw new DoiClientException(String.format( "Record '%s' is not conform with DataCite validation rules for mandatory fields. Error is: %s. " + "Required fields in DataCite are: identifier, creators, titles, publisher, publicationYear, resourceType. " + @@ -360,7 +296,7 @@ private void checkPreConditionsOnDataCite(AbstractMetadata metadata, String doi, metadata.getUuid(), e.getMessage(), sm.getNodeURL(), metadata.getUuid())) .withMessageKey("exception.doi.recordNotConformantMissingMandatory") .withDescriptionKey("exception.doi.recordNotConformantMissingMandatory.description", - new String[]{ metadata.getUuid(), e.getMessage(), sm.getNodeURL(), metadata.getUuid() }); + new String[]{metadata.getUuid(), e.getMessage(), sm.getNodeURL(), metadata.getUuid()}); } // XSD validation @@ -375,24 +311,24 @@ private void checkPreConditionsOnDataCite(AbstractMetadata metadata, String doi, metadata.getUuid(), e.getMessage(), sm.getNodeURL(), metadata.getUuid())) .withMessageKey("exception.doi.recordInvalid") .withDescriptionKey("exception.doi.recordInvalid.description", - new String[]{ metadata.getUuid(), e.getMessage(), sm.getNodeURL(), metadata.getUuid() }); + new String[]{metadata.getUuid(), e.getMessage(), sm.getNodeURL(), metadata.getUuid()}); } // * MDS / DOI does not exist already // curl -i --user username:password https://mds.test.datacite.org/doi/10.5072/GN // Return 404 - final String doiResponse = client.retrieveDoi(doi); + final String doiResponse = doiClient.retrieveDoi(doi); if (doiResponse != null) { throw new ResourceAlreadyExistException(String.format( "Record '%s' looks to be already published on DataCite with DOI '%s'. DOI on Datacite point to: %s. " + "If the DOI is not correct, remove it from the record and ask for a new one.", metadata.getUuid(), - client.createUrl("doi") + "/" + doi, + doiClient.createUrl("doi") + "/" + doi, doi, doi, doiResponse)) .withMessageKey("exception.doi.resourceAlreadyPublished") - .withDescriptionKey("exception.doi.resourceAlreadyPublished.description", new String[]{ metadata.getUuid(), - client.createUrl("doi") + "/" + doi, - doi, doi, doiResponse }); + .withDescriptionKey("exception.doi.resourceAlreadyPublished.description", new String[]{metadata.getUuid(), + doiClient.createUrl("doi") + "/" + doi, + doi, doi, doiResponse}); } // TODO: Could be relevant at some point to return states (draft/findable) @@ -404,10 +340,12 @@ private void checkPreConditionsOnDataCite(AbstractMetadata metadata, String doi, /** * Use the DataCite API to register the new DOI. + * * @param context * @param metadata */ - private void createDoi(ServiceContext context, AbstractMetadata metadata, Map doiInfo, Element dataciteMetadata) throws Exception { + private void createDoi(ServiceContext context, IDoiClient doiClient, DoiServer doiServer, + AbstractMetadata metadata, Map doiInfo, Element dataciteMetadata) throws Exception { // * Now, let's create the DOI // picking a DOI name, @@ -418,29 +356,30 @@ private void createDoi(ServiceContext context, AbstractMetadata metadata, Map doi } - public void unregisterDoi(AbstractMetadata metadata, ServiceContext context) throws DoiClientException, ResourceNotFoundException { - checkInitialised(); + public void unregisterDoi(DoiServer doiServer, AbstractMetadata metadata, ServiceContext context) throws DoiClientException, ResourceNotFoundException { + checkInitialised(doiServer); + checkCanHandleMetadata(doiServer, metadata); - final String doi = doiBuilder.create(doiPattern, doiPrefix, metadata); - final String doiResponse = client.retrieveDoi(doi); + IDoiClient doiClient = createDoiClient(doiServer); + final String doi = doiBuilder.create(doiServer.getPattern(), doiServer.getPrefix(), metadata); + final String doiResponse = doiClient.retrieveDoi(doi); if (doiResponse == null) { throw new ResourceNotFoundException(String.format( "Record '%s' is not available on DataCite. DOI '%s' does not exist.", @@ -467,12 +408,12 @@ public void unregisterDoi(AbstractMetadata metadata, ServiceContext context) thr Element md = metadata.getXmlData(false); String doiUrl = metadataUtils.getDoi(metadata.getUuid()); - client.deleteDoiMetadata(doi); - client.deleteDoi(doi); + doiClient.deleteDoiMetadata(doi); + doiClient.deleteDoi(doi); Element recordWithoutDoi = removeDOIValue(doiUrl, metadata.getDataInfo().getSchemaId(), md); - dm.updateMetadata(context, metadata.getId() + "", recordWithoutDoi, false, true, + metadataManager.updateMetadata(context, metadata.getId() + "", recordWithoutDoi, false, true, context.getLanguage(), new ISODate().toString(), true, IndexingMode.full); } catch (Exception ex) { throw new DoiClientException(ex.getMessage()); @@ -481,17 +422,16 @@ public void unregisterDoi(AbstractMetadata metadata, ServiceContext context) thr /** * Sets the DOI URL value in the metadata record using the process DOI_ADD_XSL_PROCESS. - * */ - public Element setDOIValue(String doi, String schema, Element md) throws Exception { - Path styleSheet = dm.getSchemaDir(schema).resolve(DOI_ADD_XSL_PROCESS); + public Element setDOIValue(IDoiClient doiClient, String doi, String schema, Element md) throws Exception { + Path styleSheet = schemaUtils.getSchemaDir(schema).resolve(DOI_ADD_XSL_PROCESS); boolean exists = Files.exists(styleSheet); if (!exists) { throw new DoiClientException(String.format("To create a DOI, the schema has to defined how to insert a DOI in the record. The schema_plugins/%s/process/%s was not found. Create the XSL transformation.", schema, DOI_ADD_XSL_PROCESS)); } - String doiPublicUrl = client.createPublicUrl(""); + String doiPublicUrl = doiClient.createPublicUrl(""); Map params = new HashMap<>(1); params.put("doi", doi); @@ -501,10 +441,9 @@ public Element setDOIValue(String doi, String schema, Element md) throws Excepti /** * Sets the DOI URL value in the metadata record using the process DOI_ADD_XSL_PROCESS. - * */ public Element removeDOIValue(String doi, String schema, Element md) throws Exception { - Path styleSheet = dm.getSchemaDir(schema).resolve(DOI_REMOVE_XSL_PROCESS); + Path styleSheet = schemaUtils.getSchemaDir(schema).resolve(DOI_REMOVE_XSL_PROCESS); boolean exists = Files.exists(styleSheet); if (!exists) { throw new DoiClientException(String.format("To remove a DOI, the schema has to defined how to remove a DOI in the record. The schema_plugins/%s/process/%s was not found. Create the XSL transformation.", @@ -523,24 +462,57 @@ public Element removeDOIValue(String doi, String schema, Element md) throws Exce * @return The record converted into the DataCite format. * @throws Exception if there is no conversion available. */ - private Element convertXmlToDataCiteFormat(String schema, Element md, String doi) throws Exception { - final Path styleSheet = dm.getSchemaDir(schema).resolve( - isMedra ? DATACITE_MEDRA_XSL_CONVERSION_FILE : DATACITE_XSL_CONVERSION_FILE); + private Element convertXmlToDataCiteFormat(DoiServer doiServer, String schema, Element md, String doi) throws Exception { + final Path styleSheet = schemaUtils.getSchemaDir(schema).resolve( + isMedraServer(doiServer) ? DATACITE_MEDRA_XSL_CONVERSION_FILE : DATACITE_XSL_CONVERSION_FILE); final boolean exists = Files.exists(styleSheet); if (!exists) { throw new DoiClientException(String.format("To create a DOI, the record needs to be converted to the DataCite format (https://schema.datacite.org/). You need to create a formatter for this in schema_plugins/%s/%s. If the standard is a profile of ISO19139, you can simply point to the ISO19139 formatter.", schema, DATACITE_XSL_CONVERSION_FILE)); } - Map params = new HashMap<>(); + Map params = new HashMap<>(); params.put(DOI_ID_PARAMETER, doi); return Xml.transform(md, styleSheet, params); } - private void checkInitialised() throws DoiClientException { - if (!initialised) { - throw new DoiClientException("DOI configuration is not complete. Check System Configuration and set the DOI configuration."); + private void checkInitialised(DoiServer doiServer) throws DoiClientException { + final boolean emptyUrl = StringUtils.isEmpty(doiServer.getUrl()); + final boolean emptyUsername = StringUtils.isEmpty(doiServer.getUsername()); + final boolean emptyPassword = StringUtils.isEmpty(doiServer.getPassword()); + final boolean emptyPrefix = StringUtils.isEmpty(doiServer.getPrefix()); + + if (emptyUrl || + emptyUsername || + emptyPassword || + emptyPrefix) { + throw new DoiClientException("DOI server configuration is not complete. Check DOI server and complete the configuration."); } } + /** + * Checks if the DOI server can handle the metadata: + * - The DOI server is not publishing metadata for certain metadata group(s) or + * - it publishes metadata from the metadata group owner. + * + * @param doiServer The DOI server. + * @param metadata The metadata to process. + * @throws DoiClientException + */ + private void checkCanHandleMetadata(DoiServer doiServer, AbstractMetadata metadata) throws DoiClientException { + if (!doiServer.getPublicationGroups().isEmpty()) { + Integer groupOwner = metadata.getSourceInfo().getGroupOwner(); + + if (doiServer.getPublicationGroups().stream().noneMatch(g -> g.getId() == groupOwner)) { + throw new DoiClientException( + String.format("DOI server '%s' can not handle the metadata with UUID '%s'.", + doiServer.getName(), metadata.getUuid())); + } + } + + } + + private boolean isMedraServer(DoiServer doiServer) { + return doiServer.getUrl().contains(MEDRA_SEARCH_KEY); + } } diff --git a/domain/src/main/java/org/fao/geonet/domain/DoiServer.java b/domain/src/main/java/org/fao/geonet/domain/DoiServer.java new file mode 100644 index 000000000000..90c93c31c6d4 --- /dev/null +++ b/domain/src/main/java/org/fao/geonet/domain/DoiServer.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.domain; + +import org.fao.geonet.entitylistener.DoiServerEntityListenerManager; +import org.hibernate.annotations.Type; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "Doiservers") +@Cacheable +@Access(AccessType.PROPERTY) +@EntityListeners(DoiServerEntityListenerManager.class) +@SequenceGenerator(name = DoiServer.ID_SEQ_NAME, initialValue = 100, allocationSize = 1) +public class DoiServer extends GeonetEntity { + static final String ID_SEQ_NAME = "doiserver_id_seq"; + + private int id; + private String name; + private String description; + private String url; + private String username; + private String password; + private String landingPageTemplate; + private String publicUrl; + private String pattern = "{{uuid}}"; + private String prefix; + private Set publicationGroups = new HashSet<>(); + + /** + * Get the id of the DOI server.

This is autogenerated and when a new DOI server is created + * the DOI server will be assigned a new value.

+ * + * @return the id of the DOI server. + */ + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = ID_SEQ_NAME) + @Column(nullable = false) + public int getId() { + return id; + } + + /** + * Set the id of the DOI server.

If you want to update an existing DOI server then you should + * set this id to the DOI server you want to update and set the other values to the desired + * values.

+ * + * @param id the id of the group. + * @return this DOI server object + */ + public DoiServer setId(int id) { + this.id = id; + return this; + } + + /** + * Get the basic/default name of the DOI server. This is non-translated and can be used to look + * up the DOI server like an id can.

This is a required property.

There is a max length + * to the name allowed. See the annotation for the length value.

+ * + * @return DOI server name + */ + @Column(nullable = false, length = 32) + public String getName() { + return name; + } + + /** + * Set the basic/default name of the DOI server. This is non-translated and can be used to look + * up the DOI server like an id can.

This is a required property.

There is a max length + * to the name allowed. See the annotation on {@link #getName()} for the length value.

+ */ + public DoiServer setName(String name) { + this.name = name; + return this; + } + + /** + * Get a description of the DOI server. + * + * @return the description. + */ + @Column(length = 255) + public String getDescription() { + return description; + } + + /** + * Set the DOI server description. + * + * @param description the description. + * @return this DOI server object. + */ + public DoiServer setDescription(String description) { + this.description = description; + return this; + } + + + /** + * Get the API URL for the DOI server. + * + * @return the DOI server API URL. + */ + @Column(nullable = false, length = 255) + public String getUrl() { + return url; + } + + /** + * Set the REST API configuration URL for the DOI server. + * + * @param url the server URL. + * @return this DOI server object. + */ + public DoiServer setUrl(String url) { + this.url = url; + return this; + } + + /** + * Get the username to use for connecting to the DOI server. + * + * @return the username. + */ + @Column(length = 128) + public String getUsername() { + return username; + } + + public DoiServer setUsername(String username) { + this.username = username; + return this; + } + + /** + * Get the password to use for connecting to the DOI server. + * + * @return the password. + */ + @Column(length = 128) + @Type(type="encryptedString") + public String getPassword() { + return password; + } + + public DoiServer setPassword(String password) { + this.password = password; + return this; + } + + /** + * Set the DOI landing page URL template. + * + * @param landingPageTemplate the landing page URL template. + * @return this DOI server object. + */ + public DoiServer setLandingPageTemplate(String landingPageTemplate) { + this.landingPageTemplate = landingPageTemplate; + return this; + } + + /** + * Get the DOI landing page URL template. + * + * @return the landing page URL template. + */ + @Column(nullable = false, length = 255) + public String getLandingPageTemplate() { + return landingPageTemplate; + } + + /** + * Set the DOI URL prefix. + * + * @param publicUrl the URL prefix. + * @return this DOI server object. + */ + public DoiServer setPublicUrl(String publicUrl) { + this.publicUrl = publicUrl; + return this; + } + + /** + * Get the DOI URL prefix. + * + * @return the URL prefix. + */ + @Column(nullable = false, length = 255) + public String getPublicUrl() { + return publicUrl; + } + + /** + * Set the DOI identifier pattern. + * + * @param pattern the identifier pattern. + * @return this DOI server object. + */ + public DoiServer setPattern(String pattern) { + this.pattern = pattern; + return this; + } + + /** + * Get the DOI identifier pattern. + * + * @return the identifier pattern. + */ + @Column(nullable = false, length = 255) + public String getPattern() { + return pattern; + } + + + /** + * Set the DOI prefix. + * + * @param prefix the DOI prefix. + * @return this DOI server object. + */ + public DoiServer setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + /** + * Get the DOI prefix. + * + * @return the DOI prefix. + */ + @Column(nullable = false, length = 15) + public String getPrefix() { + return prefix; + } + + /** + * Sets the groups which metadata should be published to the DOI server. + * + * @param publicationGroups Publication groups. + * @return + */ + public void setPublicationGroups(Set publicationGroups) { + this.publicationGroups = publicationGroups; + } + + /** + * Get the groups which metadata is published to the DOI server. + * + * @return Publication groups. + */ + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + @JoinTable( + name = "doiservers_group", + joinColumns = @JoinColumn(name = "doiserver_id"), + inverseJoinColumns = @JoinColumn(name = "group_id")) + public Set getPublicationGroups() { + return publicationGroups; + } +} diff --git a/domain/src/main/java/org/fao/geonet/entitylistener/DoiServerEntityListenerManager.java b/domain/src/main/java/org/fao/geonet/entitylistener/DoiServerEntityListenerManager.java new file mode 100644 index 000000000000..8d4af1bdf927 --- /dev/null +++ b/domain/src/main/java/org/fao/geonet/entitylistener/DoiServerEntityListenerManager.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.entitylistener; + +import org.fao.geonet.domain.DoiServer; + +import javax.persistence.*; + +public class DoiServerEntityListenerManager extends AbstractEntityListenerManager { + @PrePersist + public void prePresist(final DoiServer entity) { + handleEvent(PersistentEventType.PrePersist, entity); + } + + @PreRemove + public void preRemove(final DoiServer entity) { + handleEvent(PersistentEventType.PreRemove, entity); + } + + @PostPersist + public void postPersist(final DoiServer entity) { + handleEvent(PersistentEventType.PostPersist, entity); + } + + @PostRemove + public void postRemove(final DoiServer entity) { + handleEvent(PersistentEventType.PostRemove, entity); + } + + @PreUpdate + public void preUpdate(final DoiServer entity) { + handleEvent(PersistentEventType.PreUpdate, entity); + } + + @PostUpdate + public void postUpdate(final DoiServer entity) { + handleEvent(PersistentEventType.PostUpdate, entity); + } + + @PostLoad + public void postLoad(final DoiServer entity) { + handleEvent(PersistentEventType.PostLoad, entity); + } +} diff --git a/domain/src/main/java/org/fao/geonet/repository/DoiServerRepository.java b/domain/src/main/java/org/fao/geonet/repository/DoiServerRepository.java new file mode 100644 index 000000000000..25ca32429ce0 --- /dev/null +++ b/domain/src/main/java/org/fao/geonet/repository/DoiServerRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.repository; + +import org.fao.geonet.domain.DoiServer; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.Optional; + +public interface DoiServerRepository extends + GeonetRepository, + JpaSpecificationExecutor { + + Optional findOneById(int id); +} diff --git a/services/src/main/java/org/fao/geonet/api/doiservers/DoiServersApi.java b/services/src/main/java/org/fao/geonet/api/doiservers/DoiServersApi.java new file mode 100644 index 000000000000..47555a32491d --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/doiservers/DoiServersApi.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.api.doiservers; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.fao.geonet.api.ApiParams; +import org.fao.geonet.api.doiservers.model.AnonymousDoiServer; +import org.fao.geonet.api.doiservers.model.DoiServerDto; +import org.fao.geonet.api.exception.ResourceNotFoundException; +import org.fao.geonet.domain.AbstractMetadata; +import org.fao.geonet.domain.DoiServer; +import org.fao.geonet.kernel.datamanager.IMetadataUtils; +import org.fao.geonet.repository.DoiServerRepository; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequestMapping(value = { + "/{portal}/api/doiservers" +}) +@Tag(name = "doiservers", + description = "DOI servers related operations") +@RestController("doiservers") +public class DoiServersApi { + private static final String API_PARAM_DOISERVER_IDENTIFIER = "DOI server identifier"; + + private static final String API_PARAM_DOISERVER_DETAILS = "DOI server details"; + + public static final String MSG_DOISERVER_WITH_ID_NOT_FOUND = "DOI server with id '%s' not found."; + + + private final DoiServerRepository doiServerRepository; + + private final IMetadataUtils metadataUtils; + + DoiServersApi(final DoiServerRepository doiServerRepository, final IMetadataUtils metadataUtils) { + this.doiServerRepository = doiServerRepository; + this.metadataUtils = metadataUtils; + + } + + @io.swagger.v3.oas.annotations.Operation( + summary = "Get DOI servers" + ) + @GetMapping( + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + public + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "List of all DOI servers."), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) + }) + List getDoiServers() { + List doiServers = doiServerRepository.findAll(); + List list = new ArrayList<>(doiServers.size()); + doiServers.stream().forEach(e -> list.add(new AnonymousDoiServer(DoiServerDto.from(e)))); + return list; + } + + + @io.swagger.v3.oas.annotations.Operation( + summary = "Get DOI servers that can be used with a metadata" + ) + @GetMapping(value = "metadata/{metadataId}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + public + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "List of all DOI servers where a metadata can be published."), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) + }) + List getDoiServers( + @Parameter(description = "Metadata UUID", + required = true, + example = "") + @PathVariable Integer metadataId) { + + List doiServers = doiServerRepository.findAll(); + List list = new ArrayList<>(doiServers.size()); + + AbstractMetadata metadata = metadataUtils.findOne(metadataId); + Integer groupOwner = metadata.getSourceInfo().getGroupOwner(); + + // Find servers related to the metadata groups owner + List doiServersForMetadata = doiServers.stream().filter( + s -> s.getPublicationGroups().stream().anyMatch(g -> g.getId() == groupOwner)).collect(Collectors.toList()); + + if (doiServersForMetadata.isEmpty()) { + // If no servers related to the metadata groups owner, + // find the servers that are not related to any metadata group + doiServersForMetadata = doiServers.stream() + .filter(s -> s.getPublicationGroups().isEmpty()) + .collect(Collectors.toList()); + } + + doiServersForMetadata.forEach(s -> { + DoiServerDto doiServerDto = DoiServerDto.from(s); + list.add(new AnonymousDoiServer(doiServerDto)); + }); + + return list; + } + + @io.swagger.v3.oas.annotations.Operation( + summary = "Get a DOI Server" + ) + @GetMapping(value = "/{doiServerId}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_EDITOR) + }) + public AnonymousDoiServer getDoiServer( + @Parameter(description = API_PARAM_DOISERVER_IDENTIFIER, + required = true, + example = "") + @PathVariable String doiServerId + ) throws ResourceNotFoundException { + Optional doiServerOpt = doiServerRepository.findOneById(Integer.parseInt(doiServerId)); + if (doiServerOpt.isEmpty()) { + throw new ResourceNotFoundException(String.format( + MSG_DOISERVER_WITH_ID_NOT_FOUND, + doiServerId + )); + } else { + return new AnonymousDoiServer(DoiServerDto.from(doiServerOpt.get())); + } + } + + @io.swagger.v3.oas.annotations.Operation( + summary = "Add a DOI server", + description = "Return the id of the newly created DOI server." + ) + @PutMapping( + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "DOI server created."), + @ApiResponse(responseCode = "400", description = "Bad parameters."), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) + }) + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity addDoiServer( + @Parameter( + description = API_PARAM_DOISERVER_DETAILS, + required = true + ) + @RequestBody + DoiServerDto doiServerDto + ) { + Optional existingDoiServerOpt = doiServerRepository.findOneById(doiServerDto.getId()); + if (existingDoiServerOpt.isPresent()) { + throw new IllegalArgumentException(String.format( + "DOI server with id '%d' already exists.", + doiServerDto.getId() + )); + } else { + doiServerRepository.save(doiServerDto.asDoiServer()); + } + return new ResponseEntity<>(doiServerDto.getId(), HttpStatus.CREATED); + } + + @io.swagger.v3.oas.annotations.Operation( + summary = "Update a DOI server" + ) + @PutMapping( + value = "/{doiServerId}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "DOI server updated."), + @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateDoiServer( + @Parameter(description = API_PARAM_DOISERVER_IDENTIFIER, + required = true, + example = "") + @PathVariable Integer doiServerId, + @Parameter(description = API_PARAM_DOISERVER_DETAILS, + required = true) + @RequestBody + DoiServerDto doiServerDto + ) throws ResourceNotFoundException { + Optional existingMapserverOpt = doiServerRepository.findOneById(doiServerId); + if (existingMapserverOpt.isPresent()) { + DoiServer doiServer = doiServerDto.asDoiServer(); + + doiServerRepository.update(doiServerId, entity -> { + entity.setName(doiServer.getName()); + entity.setDescription(doiServer.getDescription()); + entity.setUrl(doiServer.getUrl()); + entity.setPublicUrl(doiServer.getPublicUrl()); + entity.setLandingPageTemplate(doiServer.getLandingPageTemplate()); + entity.setPattern(doiServer.getPattern()); + entity.setPrefix(doiServer.getPrefix()); + entity.setPublicationGroups(doiServer.getPublicationGroups()); + }); + } else { + throw new ResourceNotFoundException(String.format( + MSG_DOISERVER_WITH_ID_NOT_FOUND, + doiServerId + )); + } + } + + @io.swagger.v3.oas.annotations.Operation( + summary = "Remove a DOI server" + ) + @DeleteMapping( + value = "/{doiServerId}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "DOI server removed."), + @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteMapserver( + @Parameter(description = API_PARAM_DOISERVER_IDENTIFIER, + required = true + ) + @PathVariable Integer doiServerId + ) throws ResourceNotFoundException { + Optional existingMapserverOpt = doiServerRepository.findOneById(doiServerId); + if (existingMapserverOpt.isPresent()) { + doiServerRepository.delete(existingMapserverOpt.get()); + } else { + throw new ResourceNotFoundException(String.format( + MSG_DOISERVER_WITH_ID_NOT_FOUND, + doiServerId + )); + } + } + + + @io.swagger.v3.oas.annotations.Operation( + summary = "Update a DOI server authentication" + ) + @PostMapping( + value = "/{doiServerId}/auth", + produces = { + MediaType.APPLICATION_JSON_VALUE + }) + @PreAuthorize("hasAuthority('Administrator')") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "DOI server updated."), + @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), + @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) + }) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateMapserverAuth( + @Parameter( + description = API_PARAM_DOISERVER_IDENTIFIER, + required = true, + example = "") + @PathVariable Integer doiServerId, + @Parameter( + description = "User name", + required = true) + @RequestParam + String username, + @Parameter( + description = "Password", + required = true) + @RequestParam + String password + ) throws ResourceNotFoundException { + Optional existingMapserverOpt = doiServerRepository.findOneById(doiServerId); + if (existingMapserverOpt.isPresent()) { + doiServerRepository.update(doiServerId, entity -> { + entity.setUsername(username); + entity.setPassword(password); + }); + } else { + throw new ResourceNotFoundException(String.format( + MSG_DOISERVER_WITH_ID_NOT_FOUND, + doiServerId + )); + } + } +} diff --git a/services/src/main/java/org/fao/geonet/api/doiservers/model/AnonymousDoiServer.java b/services/src/main/java/org/fao/geonet/api/doiservers/model/AnonymousDoiServer.java new file mode 100644 index 000000000000..2458c8301d86 --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/doiservers/model/AnonymousDoiServer.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.api.doiservers.model; + +public class AnonymousDoiServer extends DoiServerDto { + + public AnonymousDoiServer(DoiServerDto doiServer) { + super(); + this + .setId(doiServer.getId()) + .setName(doiServer.getName()) + .setDescription(doiServer.getDescription()) + .setUrl(doiServer.getUrl()) + .setLandingPageTemplate(doiServer.getLandingPageTemplate()) + .setPattern(doiServer.getPattern()) + .setPublicUrl(doiServer.getPublicUrl()) + .setPrefix(doiServer.getPrefix()) + .setPublicationGroups(doiServer.getPublicationGroups()); + } + + @Override + public String getUsername() { + return "***"; + } + + @Override + public String getPassword() { + return "***"; + } +} diff --git a/services/src/main/java/org/fao/geonet/api/doiservers/model/DoiServerDto.java b/services/src/main/java/org/fao/geonet/api/doiservers/model/DoiServerDto.java new file mode 100644 index 000000000000..f20514181c17 --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/doiservers/model/DoiServerDto.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.api.doiservers.model; + +import org.fao.geonet.ApplicationContextHolder; +import org.fao.geonet.domain.DoiServer; +import org.fao.geonet.domain.Group; +import org.fao.geonet.repository.GroupRepository; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class DoiServerDto { + private int id; + private String name; + private String description; + private String url; + private String username; + private String password; + private String landingPageTemplate; + private String publicUrl; + private String pattern = "{{uuid}}"; + private String prefix; + private Set publicationGroups = new HashSet<>(); + + + public int getId() { + return id; + } + + public DoiServerDto setId(int id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public DoiServerDto setName(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public DoiServerDto setDescription(String description) { + this.description = description; + return this; + } + + public String getUrl() { + return url; + } + + public DoiServerDto setUrl(String url) { + this.url = url; + return this; + } + + public String getUsername() { + return username; + } + + public DoiServerDto setUsername(String username) { + this.username = username; + return this; + } + + public String getPassword() { + return password; + } + + public DoiServerDto setPassword(String password) { + this.password = password; + return this; + } + + public String getLandingPageTemplate() { + return landingPageTemplate; + } + + public DoiServerDto setLandingPageTemplate(String landingPageTemplate) { + this.landingPageTemplate = landingPageTemplate; + return this; + } + + public String getPublicUrl() { + return publicUrl; + } + + public DoiServerDto setPublicUrl(String publicUrl) { + this.publicUrl = publicUrl; + return this; + } + + public String getPattern() { + return pattern; + } + + public DoiServerDto setPattern(String pattern) { + this.pattern = pattern; + return this; + } + + public String getPrefix() { + return prefix; + } + + public DoiServerDto setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Set getPublicationGroups() { + return publicationGroups; + } + + public DoiServerDto setPublicationGroups(Set publicationGroups) { + this.publicationGroups = publicationGroups; + return this; + } + + public static DoiServerDto from(DoiServer doiServer) { + DoiServerDto doiServerDto = new DoiServerDto(); + + doiServerDto.setId(doiServer.getId()); + doiServerDto.setName(doiServer.getName()); + doiServerDto.setDescription(doiServer.getDescription()); + doiServerDto.setUrl(doiServer.getUrl()); + doiServerDto.setUsername(doiServer.getUsername()); + doiServerDto.setPassword(doiServer.getPassword()); + doiServerDto.setPattern(doiServer.getPattern()); + doiServerDto.setLandingPageTemplate(doiServer.getLandingPageTemplate()); + doiServerDto.setPublicUrl(doiServer.getPublicUrl()); + doiServerDto.setPrefix(doiServer.getPrefix()); + doiServerDto.setPublicationGroups(doiServer.getPublicationGroups().stream().map(Group::getId).collect(Collectors.toSet())); + + return doiServerDto; + } + + public DoiServer asDoiServer() { + DoiServer doiServer = new DoiServer(); + + doiServer.setId(getId()); + doiServer.setName(getName()); + doiServer.setDescription(getDescription()); + doiServer.setUrl(getUrl()); + doiServer.setUsername(getUsername()); + doiServer.setPassword(getPassword()); + doiServer.setPattern(getPattern()); + doiServer.setLandingPageTemplate(getLandingPageTemplate()); + doiServer.setPublicUrl(getPublicUrl()); + doiServer.setPrefix(getPrefix()); + + GroupRepository groupRepository = ApplicationContextHolder.get().getBean(GroupRepository.class); + Set groups = new HashSet<>(); + getPublicationGroups().forEach(groupId -> { + if (groupId != null) { + Optional g = groupRepository.findById(groupId); + + if (g.isPresent()) { + groups.add(g.get()); + } + } + }); + doiServer.setPublicationGroups(groups); + + return doiServer; + } +} diff --git a/services/src/main/java/org/fao/geonet/api/records/DoiApi.java b/services/src/main/java/org/fao/geonet/api/records/DoiApi.java index ce59aa1d8e43..5b6a803cd7fc 100644 --- a/services/src/main/java/org/fao/geonet/api/records/DoiApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/DoiApi.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -28,29 +28,28 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.context.ServiceContext; import jeeves.services.ReadWriteController; -import org.fao.geonet.api.API; import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; +import org.fao.geonet.api.exception.ResourceNotFoundException; import org.fao.geonet.doi.client.DoiManager; import org.fao.geonet.domain.AbstractMetadata; -import org.springframework.beans.factory.annotation.Autowired; +import org.fao.geonet.domain.DoiServer; +import org.fao.geonet.repository.DoiServerRepository; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Map; +import java.util.Optional; import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_OPS; import static org.fao.geonet.api.ApiParams.API_CLASS_RECORD_TAG; import static org.fao.geonet.api.ApiParams.API_PARAM_RECORD_UUID; +import static org.fao.geonet.api.doiservers.DoiServersApi.MSG_DOISERVER_WITH_ID_NOT_FOUND; /** * Handle DOI creation. @@ -60,19 +59,24 @@ }) @Tag(name = API_CLASS_RECORD_TAG, description = API_CLASS_RECORD_OPS) -@Controller("doi") +@RestController("doi") @PreAuthorize("hasAuthority('Editor')") @ReadWriteController public class DoiApi { - @Autowired - private DoiManager doiManager; + private final DoiManager doiManager; + + private final DoiServerRepository doiServerRepository; + + DoiApi(final DoiManager doiManager, final DoiServerRepository doiServerRepository) { + this.doiManager = doiManager; + this.doiServerRepository = doiServerRepository; + } @io.swagger.v3.oas.annotations.Operation( summary = "Check that a record can be submitted to DataCite for DOI creation. " + "DataCite requires some fields to be populated.") - @RequestMapping(value = "/{metadataUuid}/doi/checkPreConditions", - method = RequestMethod.GET, + @GetMapping(value = "/{metadataUuid}/doi/{doiServerId}/checkPreConditions", produces = { MediaType.APPLICATION_JSON_VALUE } @@ -86,27 +90,31 @@ public class DoiApi { @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) public - @ResponseBody ResponseEntity> checkDoiStatus( @Parameter( description = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, + @Parameter( + description = "DOI server identifier", + required = true) + @PathVariable + Integer doiServerId, @Parameter(hidden = true) HttpServletRequest request ) throws Exception { AbstractMetadata metadata = ApiUtils.canEditRecord(metadataUuid, request); ServiceContext serviceContext = ApiUtils.createServiceContext(request); - final Map reportStatus = doiManager.check(serviceContext, metadata, null); + DoiServer doiServer = retrieveDoiServer(doiServerId); + final Map reportStatus = doiManager.check(serviceContext, doiServer, metadata, null); return new ResponseEntity<>(reportStatus, HttpStatus.OK); } @io.swagger.v3.oas.annotations.Operation( summary = "Check the DOI URL created based on current configuration and pattern.") - @RequestMapping(value = "/{metadataUuid}/doi/checkDoiUrl", - method = RequestMethod.GET, + @GetMapping(value = "/{metadataUuid}/doi/{doiServerId}/checkDoiUrl", produces = { MediaType.TEXT_PLAIN_VALUE } @@ -119,26 +127,30 @@ ResponseEntity> checkDoiStatus( @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) public - @ResponseBody ResponseEntity checkDoiUrl( @Parameter( description = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, + @Parameter( + description = "DOI server identifier", + required = true) + @PathVariable + Integer doiServerId, @Parameter(hidden = true) HttpServletRequest request ) throws Exception { AbstractMetadata metadata = ApiUtils.canEditRecord(metadataUuid, request); - return new ResponseEntity<>(doiManager.checkDoiUrl(metadata), HttpStatus.OK); + DoiServer doiServer = retrieveDoiServer(doiServerId); + return new ResponseEntity<>(doiManager.checkDoiUrl(doiServer, metadata), HttpStatus.OK); } @io.swagger.v3.oas.annotations.Operation( summary = "Submit a record to the Datacite metadata store in order to create a DOI.") - @RequestMapping(value = "/{metadataUuid}/doi", - method = RequestMethod.PUT, + @PutMapping(value = "/{metadataUuid}/doi/{doiServerId}", produces = { MediaType.APPLICATION_JSON_VALUE } @@ -151,13 +163,17 @@ ResponseEntity checkDoiUrl( @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) public - @ResponseBody ResponseEntity> createDoi( @Parameter( description = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, + @Parameter( + description = "DOI server identifier", + required = true) + @PathVariable + Integer doiServerId, @Parameter(hidden = true) HttpServletRequest request, @Parameter(hidden = true) @@ -166,7 +182,8 @@ ResponseEntity> createDoi( AbstractMetadata metadata = ApiUtils.canEditRecord(metadataUuid, request); ServiceContext serviceContext = ApiUtils.createServiceContext(request); - Map doiInfo = doiManager.register(serviceContext, metadata); + DoiServer doiServer = retrieveDoiServer(doiServerId); + Map doiInfo = doiManager.register(serviceContext, doiServer, metadata); return new ResponseEntity<>(doiInfo, HttpStatus.CREATED); } @@ -174,8 +191,7 @@ ResponseEntity> createDoi( @io.swagger.v3.oas.annotations.Operation( summary = "Remove a DOI (this is not recommended, DOI are supposed to be persistent once created. This is mainly here for testing).") - @RequestMapping(value = "/{metadataUuid}/doi", - method = RequestMethod.DELETE, + @DeleteMapping(value = "/{metadataUuid}/doi/{doiServerId}", produces = { MediaType.APPLICATION_JSON_VALUE } @@ -188,8 +204,7 @@ ResponseEntity> createDoi( @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) }) public - @ResponseBody - ResponseEntity unregisterDoi( + ResponseEntity unregisterDoi( @Parameter( description = API_PARAM_RECORD_UUID, required = true) @@ -197,16 +212,34 @@ ResponseEntity unregisterDoi( String metadataUuid, @Parameter(hidden = true) HttpServletRequest request, + @Parameter( + description = "DOI server identifier", + required = true) + @PathVariable + Integer doiServerId, @Parameter(hidden = true) HttpSession session ) throws Exception { AbstractMetadata metadata = ApiUtils.canEditRecord(metadataUuid, request); ServiceContext serviceContext = ApiUtils.createServiceContext(request); - doiManager.unregisterDoi(metadata, serviceContext); + DoiServer doiServer = retrieveDoiServer(doiServerId); + doiManager.unregisterDoi(doiServer, metadata, serviceContext); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + private DoiServer retrieveDoiServer(Integer doiServerId) throws ResourceNotFoundException { + Optional doiServerOpt = doiServerRepository.findOneById(doiServerId); + if (doiServerOpt.isEmpty()) { + throw new ResourceNotFoundException(String.format( + MSG_DOISERVER_WITH_ID_NOT_FOUND, + doiServerId + )); + } + + return doiServerOpt.get(); + } + // TODO: At some point we may add support for DOI States management // https://support.datacite.org/docs/mds-api-guide#section-doi-states } diff --git a/services/src/main/java/org/fao/geonet/api/site/SiteApi.java b/services/src/main/java/org/fao/geonet/api/site/SiteApi.java index d39d42f5134d..425257a0671b 100644 --- a/services/src/main/java/org/fao/geonet/api/site/SiteApi.java +++ b/services/src/main/java/org/fao/geonet/api/site/SiteApi.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2023 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -48,7 +48,6 @@ import org.fao.geonet.api.site.model.SettingsListResponse; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.constants.Geonet; -import org.fao.geonet.doi.client.DoiManager; import org.fao.geonet.domain.*; import org.fao.geonet.exceptions.OperationAbortedEx; import org.fao.geonet.index.Status; @@ -181,8 +180,6 @@ public static void reloadServices(ServiceContext context) throws Exception { context.error(e); throw new OperationAbortedEx("Parameters saved but cannot set proxy information: " + e.getMessage()); } - DoiManager doiManager = gc.getBean(DoiManager.class); - doiManager.loadConfig(); HarvestManager harvestManager = context.getBean(HarvestManager.class); harvestManager.rescheduleActiveHarvesters(); diff --git a/web-ui/src/main/resources/catalog/components/doi/DoiDirective.js b/web-ui/src/main/resources/catalog/components/doi/DoiDirective.js index 14a49dad0481..364218dd450e 100644 --- a/web-ui/src/main/resources/catalog/components/doi/DoiDirective.js +++ b/web-ui/src/main/resources/catalog/components/doi/DoiDirective.js @@ -44,10 +44,24 @@ scope.response = {}; scope.isUpdate = angular.isDefined(scope.doiUrl); + scope.doiServers = []; + scope.selectedDoiServer = null; + + gnDoiService.getDoiServersForMetadata(scope.uuid).then(function (response) { + scope.doiServers = response.data; + if (scope.doiServers.length > 0) { + scope.selectedDoiServer = scope.doiServers[0].id; + } + }); + + scope.updateDoiServer = function () { + scope.response = {}; + }; + scope.check = function () { scope.response = {}; scope.response["check"] = null; - return gnDoiService.check(scope.uuid).then( + return gnDoiService.check(scope.uuid, scope.selectedDoiServer).then( function (r) { scope.response["check"] = r; scope.isUpdate = angular.isDefined(scope.doiUrl); @@ -60,7 +74,7 @@ }; scope.create = function () { - return gnDoiService.create(scope.uuid).then( + return gnDoiService.create(scope.uuid, scope.selectedDoiServer).then( function (r) { scope.response["create"] = r; delete scope.response["check"]; diff --git a/web-ui/src/main/resources/catalog/components/doi/DoiService.js b/web-ui/src/main/resources/catalog/components/doi/DoiService.js index e21427f8c31d..cee4d27b3ff8 100644 --- a/web-ui/src/main/resources/catalog/components/doi/DoiService.js +++ b/web-ui/src/main/resources/catalog/components/doi/DoiService.js @@ -33,11 +33,39 @@ "$http", "gnConfig", function ($http, gnConfig) { - function check(id) { - return $http.get("../api/records/" + id + "/doi/checkPreConditions"); + /** + * Returns a promise to validate a metadata to be published on a DOI server. + * + * @param id + * @param doiServerId + * @returns {*} + */ + function check(id, doiServerId) { + return $http.get( + "../api/records/" + id + "/doi/" + doiServerId + "/checkPreConditions" + ); } - function create(id) { - return $http.put("../api/records/" + id + "/doi"); + + /** + * Returns a promise to publish a metadata on a DOI server. + * + * @param id + * @param doiServerId + * @returns {*} + */ + function create(id, doiServerId) { + return $http.put("../api/records/" + id + "/doi/" + doiServerId); + } + + /** + * Returns a promise to retrieve the list of DOI servers + * where a metadata can be published. + * + * @param metadataId + * @returns {*} + */ + function getDoiServersForMetadata(metadataId) { + return $http.get("../api/doiservers/metadata/" + metadataId); } function isDoiApplicableForMetadata(md) { @@ -73,7 +101,8 @@ check: check, create: create, isDoiApplicableForMetadata: isDoiApplicableForMetadata, - canPublishDoiForResource: canPublishDoiForResource + canPublishDoiForResource: canPublishDoiForResource, + getDoiServersForMetadata: getDoiServersForMetadata }; } ]); diff --git a/web-ui/src/main/resources/catalog/components/doi/partials/doiwidget.html b/web-ui/src/main/resources/catalog/components/doi/partials/doiwidget.html index c1a6a8dc3bc3..2edf7718127c 100644 --- a/web-ui/src/main/resources/catalog/components/doi/partials/doiwidget.html +++ b/web-ui/src/main/resources/catalog/components/doi/partials/doiwidget.html @@ -5,6 +5,16 @@

createDoiForRecord

+ + +
+ + +
+ + +
+
+
+ updateDoiServer + newDoiServer + {{doiServerSelected.name}} +
+ + + +
+
+
+
+ + + +
+ + +
+ + +
+

fieldRequired

+
+
+
+ +
+ + +
+ +
+
+ +
+ + +
+ + +
+

fieldRequired

+
+
+ +
+

doiserver-url-help

+
+
+ +
+
+ + +
+ + +
+

fieldRequired

+
+
+
+ +
+ + +
+ + +
+

fieldRequired

+
+
+ +
+

doiserver-password-help

+
+
+
+ +
+ + +
+ + +
+

fieldRequired

+
+
+ +
+

+ doiserver-landingPageTemplate-help +

+
+
+ +
+ + +
+ +
+ +
+

doiserver-publicUrl-help

+
+
+ +
+ + +
+ + +
+

fieldRequired

+
+
+ +
+

doiserver-pattern-help

+
+
+ +
+ + +
+ + +
+

fieldRequired

+
+
+ +
+

doiserver-prefix-help

+
+
+ +
+ +
+
+
+ +
+

doiserver-publicationGroups-help

+
+
+
+
+
+
+ + + +
+

confirmDoiServerDelete

+
+ diff --git a/web-ui/src/main/resources/catalog/views/default/directives/directive.js b/web-ui/src/main/resources/catalog/views/default/directives/directive.js index 3cb6d2575019..37ec7662bc3e 100644 --- a/web-ui/src/main/resources/catalog/views/default/directives/directive.js +++ b/web-ui/src/main/resources/catalog/views/default/directives/directive.js @@ -98,6 +98,7 @@ module.directive("gnMdActionsMenu", [ "gnMetadataActions", "$http", + "$q", "gnConfig", "gnConfigService", "gnGlobalSettings", @@ -105,6 +106,7 @@ function ( gnMetadataActions, $http, + $q, gnConfig, gnConfigService, gnGlobalSettings, @@ -122,6 +124,12 @@ scope.tasks = []; scope.hasVisibletasks = false; + scope.doiServers = []; + + $http.get("../api/doiservers").then(function (response) { + scope.doiServers = response.data; + }); + gnConfigService.load().then(function (c) { scope.isMdWorkflowEnable = gnConfig["metadata.workflow.enable"]; @@ -224,7 +232,16 @@ scope.taskConfiguration = { doiCreationTask: { isVisible: function (md) { - return gnConfig["system.publication.doi.doienabled"]; + var hasDoiServer = false; + + for (var i = 0; i < scope.doiServers.length; i++) { + if (scope.doiServers[i].defaultServer) { + hasDoiServer = true; + break; + } + } + + return hasDoiServer; }, isApplicable: function (md) { // TODO: Would be good to return why a task is not applicable as tooltip diff --git a/web/src/main/webResources/WEB-INF/config-db/database_migration.xml b/web/src/main/webResources/WEB-INF/config-db/database_migration.xml index 1e3fb2025e63..1b1cb2deb32e 100644 --- a/web/src/main/webResources/WEB-INF/config-db/database_migration.xml +++ b/web/src/main/webResources/WEB-INF/config-db/database_migration.xml @@ -391,6 +391,7 @@ + java:v445.DoiServerDatabaseMigration WEB-INF/classes/setup/sql/migrate/v445/migrate- diff --git a/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql b/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql index 0e84082618eb..b1f7bceaabc8 100644 --- a/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql +++ b/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql @@ -723,16 +723,6 @@ INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/publickey', '', 0, 1910, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/secretkey', '', 0, 1910, 'y'); - -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doienabled', 'false', 2, 100191, 'n'); -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doiurl', '', 0, 100192, 'n'); -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doiusername', '', 0, 100193, 'n'); -INSERT INTO Settings (name, value, datatype, position, internal, encrypted) VALUES ('system/publication/doi/doipassword', '', 0, 100194, 'y', 'y'); -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doikey', '', 0, 110095, 'n'); -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doilandingpagetemplate', 'http://localhost:8080/geonetwork/srv/resources/records/{{uuid}}', 0, 100195, 'n'); -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doipublicurl', '', 0, 100196, 'n'); -INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doipattern', '{{uuid}}', 0, 100197, 'n'); - INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/security/passwordEnforcement/minLength', '6', 1, 12000, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/security/passwordEnforcement/maxLength', '20', 1, 12001, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/security/passwordEnforcement/usePattern', 'true', 2, 12002, 'n'); diff --git a/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v445/DoiServerDatabaseMigration.java b/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v445/DoiServerDatabaseMigration.java new file mode 100644 index 000000000000..bc1408c0a0dd --- /dev/null +++ b/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v445/DoiServerDatabaseMigration.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2001-2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package v445; + +import org.fao.geonet.DatabaseMigrationTask; +import org.fao.geonet.constants.Geonet; +import org.fao.geonet.migration.DatabaseMigrationException; +import org.fao.geonet.utils.Log; +import org.springframework.util.StringUtils; + + +import java.sql.*; + +public class DoiServerDatabaseMigration extends DatabaseMigrationTask { + @Override + public void update(Connection connection) throws SQLException, DatabaseMigrationException { + Log.debug(Geonet.DB, "DoiServerDatabaseMigration"); + + boolean doiEnabled = false; + String doiUrl = ""; + String doiUsername = ""; + String doiPassword = ""; + String doiKey = ""; + String doiLandingPageTemplate = ""; + String doiPublicUrl = ""; + String doiPattern = ""; + + try (Statement statement = connection.createStatement()) { + final String selectDoiSerttingsSQL = "SELECT name, value FROM Settings WHERE name LIKE 'system/publication/doi%'"; + + String columnForName = "name"; + String columnForValue = "value"; + + final ResultSet resultSet = statement.executeQuery(selectDoiSerttingsSQL); + while (resultSet.next()) { + if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doienabled")) { + doiEnabled = resultSet.getString(columnForValue).equalsIgnoreCase("true"); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doiurl")) { + doiUrl = resultSet.getString(columnForValue); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doiusername")) { + doiUsername = resultSet.getString(columnForValue); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doipassword")) { + doiPassword = resultSet.getString(columnForValue); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doikey")) { + doiKey = resultSet.getString(columnForValue); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doilandingpagetemplate")) { + doiLandingPageTemplate = resultSet.getString(columnForValue); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doipublicurl")) { + doiPublicUrl = resultSet.getString(columnForValue); + } else if (resultSet.getString(columnForName).equalsIgnoreCase("system/publication/doi/doipattern")) { + doiPattern = resultSet.getString(columnForValue); + } + + } + } + + if (doiEnabled) { + + // Check the information is filled + boolean createDoiServer = StringUtils.hasLength(doiUrl) && + StringUtils.hasLength(doiUsername) && + StringUtils.hasLength(doiPassword) && + StringUtils.hasLength(doiKey) && + StringUtils.hasLength(doiPattern); + + if (createDoiServer) { + try (PreparedStatement update = connection.prepareStatement( + "INSERT INTO doiservers " + + "(id, isdefault, landingpagetemplate, name, url, username, password, pattern, prefix, publicurl) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + ) { + + update.setInt(1, 1); + update.setString(2, "y"); + update.setString(3, doiLandingPageTemplate); + update.setString(4, "Default DOI server"); + update.setString(5, doiUrl); + update.setString(6, doiUsername); + update.setString(7, doiPassword); + update.setString(8, doiPattern); + update.setString(9, doiKey); + update.setString(10, doiPublicUrl); + + update.execute(); + + } catch (java.sql.BatchUpdateException e) { + connection.rollback(); + Log.error(Geonet.GEONETWORK, "Error occurred while creating the DOI server:" + e.getMessage(), e); + SQLException next = e.getNextException(); + while (next != null) { + Log.error(Geonet.GEONETWORK, "Next error: " + next.getMessage(), next); + next = e.getNextException(); + } + + throw new RuntimeException(e); + } catch (Exception e) { + connection.rollback(); + + throw new Error(e); + } + + + try (PreparedStatement delete = connection.prepareStatement( + "DELETE FROM Settings WHERE name LIKE 'system/publication/doi%'") + ) { + delete.execute(); + } catch (java.sql.BatchUpdateException e) { + connection.rollback(); + Log.error(Geonet.GEONETWORK, "Error occurred while creating the DOI server:" + e.getMessage(), e); + SQLException next = e.getNextException(); + while (next != null) { + Log.error(Geonet.GEONETWORK, "Next error: " + next.getMessage(), next); + next = e.getNextException(); + } + + throw new RuntimeException(e); + } catch (Exception e) { + connection.rollback(); + + throw new Error(e); + } + + connection.commit(); + + Log.info(Geonet.DB, "Migration: migrated DOI server"); + } + } + } +}