diff --git a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java index a9661213016e..e8906cb8b5d5 100644 --- a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java +++ b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java @@ -109,6 +109,7 @@ public class Settings { public static final String SYSTEM_HARVESTER_DISABLED_HARVESTER_TYPES = "system/harvester/disabledHarvesterTypes"; public static final String SYSTEM_METADATAPRIVS_USERGROUPONLY = "system/metadataprivs/usergrouponly"; public static final String SYSTEM_METADATAPRIVS_PUBLICATIONBYGROUPOWNERONLY = "system/metadataprivs/publicationbyrevieweringroupowneronly"; + public static final String SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE = "system/metadataprivs/publication/managepublicationdate"; public static final String SYSTEM_METADATAPRIVS_PUBLICATIONNOTIFICATION_EMAILS = "system/metadataprivs/publication/notificationEmails"; public static final String SYSTEM_METADATAPRIVS_PUBLICATION_NOTIFICATIONLEVEL = "system/metadataprivs/publication/notificationLevel"; public static final String SYSTEM_METADATAPRIVS_PUBLICATION_NOTIFICATIONGROUPS = "system/metadataprivs/publication/notificationGroups"; diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 6463f93676d2..6b6db3d86be5 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -249,7 +249,11 @@ Note: this option is only available for databases that have been tested. Those d ## Metadata Privileges -*Only set privileges to user's groups*: If enabled then only the groups that the user belongs to will be displayed in the metadata privileges page (unless the user is an Administrator). At the moment this option cannot be disabled and is likely to be deprecated in the next version of GeoNetwork. +- **Only set privileges to user's groups**: If enabled then only the groups that the user belongs to will be displayed in the metadata privileges page (unless the user is an Administrator). +- **Publication by users reviewer in record group only**: Allow publication by administrator and reviewer member of record group. If disabled, then also all users reviewer in group with editing rights can publish/unpublish a record. +- **Manage the publication date automatically**: When enabled the publication date of the metadata is set automatically when the metadata is published and removed when the metadata is unpublished. +- **Notification level when a metadata is published / unpublished**: Define which users to alert when a metadata is published / unpublished. +- **Groups to notify when a metadata is published / unpublished**: List of groups, separated by the char |, to notify when a metadata is published / unpublished (for 'Notify the group(s) emails' notification level). ## Metadata create diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl index f9d22dd23a24..1ab6c59b826d 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl @@ -213,6 +213,14 @@ + + + + + + diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/process/publicationdate-add.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/process/publicationdate-add.xsl new file mode 100644 index 000000000000..1a6bee0b6226 --- /dev/null +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/process/publicationdate-add.xsl @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/process/publicationdate-remove.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/process/publicationdate-remove.xsl new file mode 100644 index 000000000000..618be7ff4e41 --- /dev/null +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/process/publicationdate-remove.xsl @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + diff --git a/schemas/iso19115-3.2018/src/test/java/org/fao/geonet/schemas/PublicationDateAddProcessTest.java b/schemas/iso19115-3.2018/src/test/java/org/fao/geonet/schemas/PublicationDateAddProcessTest.java new file mode 100644 index 000000000000..0c48eaacbb23 --- /dev/null +++ b/schemas/iso19115-3.2018/src/test/java/org/fao/geonet/schemas/PublicationDateAddProcessTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2001-2026 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.schemas; + +import org.fao.geonet.schema.iso19115_3_2018.ISO19115_3_2018SchemaPlugin; +import org.fao.geonet.utils.Xml; +import org.jdom.Element; +import org.jdom.Namespace; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.xmlunit.matchers.EvaluateXPathMatcher.hasXPath; + +public class PublicationDateAddProcessTest extends XslProcessTest { + + public PublicationDateAddProcessTest() { + super(); + this.setXslFilename("process/publicationdate-add.xsl"); + this.setXmlFilename("metadata.xml"); + this.setNs(ISO19115_3_2018SchemaPlugin.allNamespaces); + } + + @Test + public void mustNotAlterARecordWhenNoParameterProvided() throws Exception { + super.testMustNotAlterARecordWhenNoParameterProvided(); + } + + @Test + public void testAddPublicationDate() throws Exception { + Element inputElement = Xml.loadFile(xmlFile); + String inputString = Xml.getString(inputElement); + + // Check no publication date exists + assertThat( + inputString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("0")).withNamespaceContext(ns) + ); + + // Add publication date + Map params = new HashMap<>(); + String newDate = "2026-07-02"; + params.put("publicationDate", newDate); + + Element resultElement = Xml.transform(inputElement, xslFile, params); + String resultString = Xml.getString(resultElement); + + assertThat( + resultString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("1")).withNamespaceContext(ns) + ); + assertThat( + resultString, hasXPath("//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']//gco:Date/text()", equalTo(newDate)).withNamespaceContext(ns) + ); + } + + @Test + public void testReplacePublicationDate() throws Exception { + Element inputElement = Xml.loadFile(xmlFile); + + // 1. Add a publication date first + Map params = new HashMap<>(); + params.put("publicationDate", "2020-01-01"); + Element midElement = Xml.transform(inputElement, xslFile, params); + String midString = Xml.getString(midElement); + + assertThat( + midString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("1")).withNamespaceContext(ns) + ); + + // 2. Replace it with a new one + String newDate = "2026-07-02"; + params.put("publicationDate", newDate); + Element resultElement = Xml.transform(midElement, xslFile, params); + String resultString = Xml.getString(resultElement); + + assertThat( + resultString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("1")).withNamespaceContext(ns) + ); + assertThat( + resultString, hasXPath("//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']//gco:Date/text()", equalTo(newDate)).withNamespaceContext(ns) + ); + } + + @Test + public void testReplaceMultiplePublicationDates() throws Exception { + // Create an input with 2 publication dates + Element inputElement = Xml.loadFile(xmlFile); + + Map params = new HashMap<>(); + params.put("publicationDate", "2020-01-01"); + Element midElement = Xml.transform(inputElement, xslFile, params); + + // Add another publication date manually at metadata level (mdb:dateInfo). + // The transformed root element is mdb:MD_Metadata. + midElement.addContent(newDateInfo(midElement.getNamespace(), "publication", "2021-01-01")); + + String midStringWithTwo = Xml.getString(midElement); + assertThat( + midStringWithTwo, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("2")).withNamespaceContext(ns) + ); + + // Now run the process + String newDate = "2026-07-02T09:23:00"; + params.put("publicationDate", newDate); + Element resultElement = Xml.transform(midElement, xslFile, params); + String resultString = Xml.getString(resultElement); + + assertThat( + resultString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("1")).withNamespaceContext(ns) + ); + assertThat( + resultString, hasXPath("//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']//gco:Date/text()", equalTo(newDate)).withNamespaceContext(ns) + ); + } + + /** + * Regression test: a metadata date whose CI_DateTypeCode has no codeListValue attribute + * must be preserved when a publication date is added. A naive {@code @codeListValue != 'publication'} + * predicate silently drops such dates (an absent attribute is not "!= 'publication'" in XPath), + * so this guards against that data loss. + */ + @Test + public void testPreservesDateWithoutCodeListValueWhenAddingPublicationDate() throws Exception { + Element inputElement = Xml.loadFile(xmlFile); + + // Add a metadata date with a CI_DateTypeCode that has no codeListValue attribute. + // The loaded root element is mdb:MD_Metadata. + inputElement.addContent(newDateInfo(inputElement.getNamespace(), null, "2019-05-05")); + + String inputString = Xml.getString(inputElement); + assertThat( + inputString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode[not(@codeListValue)]])", equalTo("1")).withNamespaceContext(ns) + ); + + // Add the publication date + Map params = new HashMap<>(); + params.put("publicationDate", "2026-07-02"); + Element resultElement = Xml.transform(inputElement, xslFile, params); + String resultString = Xml.getString(resultElement); + + // The publication date is added ... + assertThat( + resultString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("1")).withNamespaceContext(ns) + ); + // ... and the date without a codeListValue is preserved (not dropped). + assertThat( + resultString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode[not(@codeListValue)]])", equalTo("1")).withNamespaceContext(ns) + ); + } + + /** + * Build an {@code mdb:dateInfo} element with the given date type code list value + * (omitted when {@code codeListValue} is null) and date time value. + */ + private Element newDateInfo(Namespace mdbNs, String codeListValue, String dateTime) { + Namespace citNs = Namespace.getNamespace("cit", ns.get("cit")); + Namespace gcoNs = Namespace.getNamespace("gco", ns.get("gco")); + + Element dateInfo = new Element("dateInfo", mdbNs); + Element ciDate = new Element("CI_Date", citNs); + + Element dateVal = new Element("date", citNs); + Element dateTimeEl = new Element("Date", gcoNs); + dateTimeEl.setText(dateTime); + dateVal.addContent(dateTimeEl); + ciDate.addContent(dateVal); + + Element dateType = new Element("dateType", citNs); + Element dateTypeCode = new Element("CI_DateTypeCode", citNs); + if (codeListValue != null) { + dateTypeCode.setAttribute("codeListValue", codeListValue); + dateTypeCode.setAttribute("codeList", "codeListLocation#CI_DateTypeCode"); + } + dateType.addContent(dateTypeCode); + ciDate.addContent(dateType); + + dateInfo.addContent(ciDate); + return dateInfo; + } +} diff --git a/schemas/iso19115-3.2018/src/test/java/org/fao/geonet/schemas/PublicationDateRemoveProcessTest.java b/schemas/iso19115-3.2018/src/test/java/org/fao/geonet/schemas/PublicationDateRemoveProcessTest.java new file mode 100644 index 000000000000..9fc047020057 --- /dev/null +++ b/schemas/iso19115-3.2018/src/test/java/org/fao/geonet/schemas/PublicationDateRemoveProcessTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2001-2026 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.schemas; + +import org.fao.geonet.schema.iso19115_3_2018.ISO19115_3_2018SchemaPlugin; +import org.fao.geonet.utils.Xml; +import org.jdom.Element; +import org.jdom.Namespace; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.xmlunit.matchers.EvaluateXPathMatcher.hasXPath; + +public class PublicationDateRemoveProcessTest extends XslProcessTest { + + public PublicationDateRemoveProcessTest() { + super(); + this.setXslFilename("process/publicationdate-remove.xsl"); + this.setXmlFilename("metadata.xml"); + this.setNs(ISO19115_3_2018SchemaPlugin.allNamespaces); + } + + @Test + public void testRemovePublicationDate() throws Exception { + Element inputElement = Xml.loadFile(xmlFile); + + Namespace cit = Namespace.getNamespace("cit", "http://standards.iso.org/iso/19115/-3/cit/2.0"); + Namespace gco = Namespace.getNamespace("gco", "http://standards.iso.org/iso/19115/-3/gco/1.0"); + + // Add a metadata-level publication date (mdb:dateInfo). The loaded root is mdb:MD_Metadata. + Element dateInfo = new Element("dateInfo", inputElement.getNamespace()); + Element ciDate = new Element("CI_Date", cit); + Element dateEl = new Element("date", cit); + Element date = new Element("Date", gco).setText("2026-07-02"); + Element dateType = new Element("dateType", cit); + Element ciDateTypeCode = new Element("CI_DateTypeCode", cit) + .setAttribute("codeList", "codeListLocation#CI_DateTypeCode") + .setAttribute("codeListValue", "publication"); + + dateEl.addContent(date); + dateType.addContent(ciDateTypeCode); + ciDate.addContent(dateEl); + ciDate.addContent(dateType); + dateInfo.addContent(ciDate); + inputElement.addContent(dateInfo); + + String midString = Xml.getString(inputElement); + + Map xslNs = new HashMap<>(); + xslNs.put("mdb", "http://standards.iso.org/iso/19115/-3/mdb/2.0"); + xslNs.put("cit", "http://standards.iso.org/iso/19115/-3/cit/2.0"); + + assertThat( + midString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("1")).withNamespaceContext(xslNs) + ); + + // 2. Now remove it + Element resultElement = Xml.transform(inputElement, xslFile); + String resultString = Xml.getString(resultElement); + + assertThat( + resultString, hasXPath("count(//mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication'])", equalTo("0")).withNamespaceContext(xslNs) + ); + } +} diff --git a/schemas/schema-core/src/test/java/org/fao/geonet/schemas/XslProcessTest.java b/schemas/schema-core/src/test/java/org/fao/geonet/schemas/XslProcessTest.java index 03c02cc421da..c9dbc7824c35 100644 --- a/schemas/schema-core/src/test/java/org/fao/geonet/schemas/XslProcessTest.java +++ b/schemas/schema-core/src/test/java/org/fao/geonet/schemas/XslProcessTest.java @@ -56,7 +56,7 @@ public abstract class XslProcessTest { - protected Map ns = new HashMap(); + protected Map ns = new HashMap<>(); public Map getNs() { return ns; } diff --git a/services/src/main/java/org/fao/geonet/api/records/events/MetadataRemovePublicationDateListener.java b/services/src/main/java/org/fao/geonet/api/records/events/MetadataRemovePublicationDateListener.java new file mode 100644 index 000000000000..1bb2ad990a9c --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/records/events/MetadataRemovePublicationDateListener.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2001-2026 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.records.events; + +import org.fao.geonet.domain.AbstractMetadata; +import org.fao.geonet.domain.Metadata; +import org.fao.geonet.domain.MetadataType; +import org.fao.geonet.events.md.MetadataUnpublished; +import org.fao.geonet.kernel.datamanager.IMetadataUtils; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; +import org.fao.geonet.utils.Log; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; + +public class MetadataRemovePublicationDateListener implements ApplicationListener { + + @Autowired + private MetadataUpdatePublicationDateService metadataUpdatePublicationDateService; + + @Autowired + private SettingManager settingManager; + + @Autowired + private IMetadataUtils metadataUtils; + + @Override + public void onApplicationEvent(MetadataUnpublished event) { + if (!settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)) { + return; + } + + AbstractMetadata md = event.getMd(); + // On republication (approval workflow), the metadata received in the event is the draft + // version. The publication date must be removed from the approved record, not the draft. + if (!(md instanceof Metadata)) { + md = metadataUtils.findOneByUuid(md.getUuid()); + } + if (md == null || md.getDataInfo().getType() != MetadataType.METADATA) { + return; + } + + try { + metadataUpdatePublicationDateService.removePublicationDate(md); + } catch (Exception e) { + Log.error("org.fao.geonet.services.metadata", "Error removing publication date of metadata " + md.getId(), e); + } + } + +} + diff --git a/services/src/main/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateListener.java b/services/src/main/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateListener.java new file mode 100644 index 000000000000..3f292d4f9650 --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateListener.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2001-2026 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.records.events; + +import org.fao.geonet.domain.AbstractMetadata; +import org.fao.geonet.domain.ISODate; +import org.fao.geonet.domain.Metadata; +import org.fao.geonet.domain.MetadataType; +import org.fao.geonet.events.md.MetadataPublished; +import org.fao.geonet.kernel.datamanager.IMetadataUtils; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; +import org.fao.geonet.utils.Log; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; + +public class MetadataUpdatePublicationDateListener implements ApplicationListener { + + @Autowired + private MetadataUpdatePublicationDateService metadataUpdatePublicationDateService; + + @Autowired + private SettingManager settingManager; + + @Autowired + private IMetadataUtils metadataUtils; + + @Override + public void onApplicationEvent(MetadataPublished event) { + if (!settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)) { + return; + } + + AbstractMetadata md = event.getMd(); + // On republication (approval workflow), the metadata received in the event is the draft + // version. The publication date must be set on the approved record, not the draft. + if (!(md instanceof Metadata)) { + md = metadataUtils.findOneByUuid(md.getUuid()); + } + if (md == null || md.getDataInfo().getType() != MetadataType.METADATA) { + return; + } + + try { + metadataUpdatePublicationDateService.addPublicationDate(md, new ISODate()); + } catch (Exception e) { + Log.error("org.fao.geonet.services.metadata", "Error setting publication date of metadata " + md.getId(), e); + } + } + +} + diff --git a/services/src/main/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateService.java b/services/src/main/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateService.java new file mode 100644 index 000000000000..143f43fb98e7 --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateService.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2001-2026 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.records.events; + +import jeeves.server.context.ServiceContext; +import org.fao.geonet.api.processing.XslProcessUtils; +import org.fao.geonet.api.processing.report.Report; +import org.fao.geonet.api.processing.report.XsltMetadataProcessingReport; +import org.fao.geonet.domain.AbstractMetadata; +import org.fao.geonet.domain.ISODate; +import org.fao.geonet.kernel.SchemaManager; +import org.springframework.stereotype.Service; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class MetadataUpdatePublicationDateService { + public void addPublicationDate(AbstractMetadata md, ISODate publicationDate) { + + ServiceContext context = ServiceContext.get(); + + String process = "publicationdate-add"; + if (!hasProcess(context, md, process)) { + // The record schema does not provide the process (only some schemas + // such as ISO19115-3 do), so publication date management does not apply. + return; + } + + XsltMetadataProcessingReport report = + new XsltMetadataProcessingReport(process); + + Map params = new HashMap<>(); + params.put("publicationDate", new String[]{publicationDate.getDateAsString()}); + try { + XslProcessUtils.process(context, String.valueOf(md.getId()), process, true, true, true, report, "", params); + } catch (Exception e) { + throw new RuntimeException(e); + } + checkForErrors(report, md); + } + + public void removePublicationDate(AbstractMetadata md) { + + ServiceContext context = ServiceContext.get(); + + String process = "publicationdate-remove"; + if (!hasProcess(context, md, process)) { + // The record schema does not provide the process (only some schemas + // such as ISO19115-3 do), so publication date management does not apply. + return; + } + + XsltMetadataProcessingReport report = + new XsltMetadataProcessingReport(process); + + Map params = new HashMap<>(); + + try { + XslProcessUtils.process(context, String.valueOf(md.getId()), process, true, true, true, report, "", params); + } catch (Exception e) { + throw new RuntimeException(e); + } + checkForErrors(report, md); + } + + /** + * The publication date processes are only provided by some schema plugins (eg. ISO19115-3). + * Records whose schema does not provide the process are skipped so that publishing records + * of other schemas is not affected. + */ + private boolean hasProcess(ServiceContext context, AbstractMetadata md, String process) { + SchemaManager schemaManager = context.getBean(SchemaManager.class); + String schema = md.getDataInfo().getSchemaId(); + Path xslProcessing = schemaManager.getSchemaDir(schema).resolve("process").resolve(process + ".xsl"); + return Files.exists(xslProcessing); + } + + /** + * XslProcessUtils.process() catches its own processing exceptions and records them on the + * report instead of throwing, so a failure to set/remove the publication date would otherwise + * pass completely unnoticed. Surface it here so the caller can log/handle it. + */ + private void checkForErrors(XsltMetadataProcessingReport report, AbstractMetadata md) { + if (report.getNumberOfRecordsWithErrors() > 0) { + List errors = report.getMetadataErrors().values().stream() + .flatMap(List::stream).collect(Collectors.toList()); + String messages = errors.stream().map(Report::getMessage).collect(Collectors.joining("; ")); + throw new RuntimeException("Failed to process publication date XSL for metadata " + + md.getId() + ": " + messages); + } + } +} diff --git a/services/src/main/resources/config-spring-geonetwork.xml b/services/src/main/resources/config-spring-geonetwork.xml index 830a19585067..2d61df66bbf5 100644 --- a/services/src/main/resources/config-spring-geonetwork.xml +++ b/services/src/main/resources/config-spring-geonetwork.xml @@ -123,6 +123,10 @@ class="org.fao.geonet.api.records.formatters.cache.FormatterCacheDeletionListener"/> + + diff --git a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java index e5ee8d9d1293..55a037741363 100644 --- a/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/records/MetadataSharingApiTest.java @@ -40,13 +40,18 @@ import org.fao.geonet.domain.UserGroup; import org.fao.geonet.domain.MetadataStatus; import org.fao.geonet.domain.StatusValue; +import org.fao.geonet.domain.ISODate; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.kernel.setting.Settings; import org.fao.geonet.repository.MetadataRepository; import org.fao.geonet.repository.MetadataStatusRepository; import org.fao.geonet.repository.OperationAllowedRepository; import org.fao.geonet.repository.UserRepositoryTest; +import org.fao.geonet.schema.iso19115_3_2018.ISO19115_3_2018SchemaPlugin; import org.fao.geonet.services.AbstractServiceIntegrationTest; +import org.fao.geonet.utils.Xml; +import org.jdom.Element; +import org.jdom.Namespace; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -57,6 +62,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -64,6 +71,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @@ -75,6 +83,15 @@ **/ public class MetadataSharingApiTest extends AbstractServiceIntegrationTest { private static final int SAMPLE_GROUP_ID = 2; + + /** + * Selects the metadata-level publication date (mdb:dateInfo) added by the + * ISO19115-3.2018 {@code publicationdate-add} process. This does not match the + * resource-level publication date carried in the sample's identification info. + */ + private static final String PUBLICATION_DATE_INFO_XPATH = + "mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']"; + @Autowired private WebApplicationContext wac; @@ -90,6 +107,9 @@ public class MetadataSharingApiTest extends AbstractServiceIntegrationTest { @Autowired private SettingManager settingManager; + @PersistenceContext + private EntityManager entityManager; + private User editorUser; private User reviewerUser; private int metadataId; @@ -631,6 +651,100 @@ private User createUserAdminWithGroupProfile(String username, Profile groupProfi return user; } + /** + * When {@code system/metadataprivs/publication/managepublicationdate} is enabled, publishing an + * ISO19115-3.2018 record must set the metadata-level publication date in the stored XML using the + * schema's {@code publicationdate-add} process. + */ + @Test + public void publishIso191153UpdatesPublicationDateWhenManageEnabled() throws Exception { + settingManager.setValue(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE, true); + // Do not let validation block publication of the sample record; this test targets the publication date only. + settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_INVALID_MD, true); + + int isoId = injectIso191153Record(); + String isoUuid = metadataRepository.findById(isoId).get().getUuid(); + + List ns = new ArrayList<>(ISO19115_3_2018SchemaPlugin.allNamespaces); + + // The sample has no metadata-level publication date before publishing. + Element beforeXml = metadataRepository.findById(isoId).get().getXmlData(false); + assertEquals(0, Xml.selectNodes(beforeXml, PUBLICATION_DATE_INFO_XPATH, ns).size()); + + String expectedDate = new ISODate().getDateAsString(); + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + MockHttpSession mockHttpSession = loginAs(reviewerUser); + mockMvc.perform(put("/srv/api/records/" + isoUuid + "/publish") + .session(mockHttpSession)) + .andExpect(status().isNoContent()); + + // Re-read the stored record from the database. + entityManager.flush(); + entityManager.clear(); + Element afterXml = metadataRepository.findById(isoId).get().getXmlData(false); + + assertEquals("A single metadata-level publication date should be present after publishing", + 1, Xml.selectNodes(afterXml, PUBLICATION_DATE_INFO_XPATH, ns).size()); + Element publicationDate = (Element) Xml.selectSingle(afterXml, + PUBLICATION_DATE_INFO_XPATH + "/cit:CI_Date/cit:date/gco:Date", ns); + assertNotNull("Publication date value should be set", publicationDate); + assertEquals("Publication date should be the date of publication (today)", + expectedDate, publicationDate.getText()); + } + + /** + * When {@code system/metadataprivs/publication/managepublicationdate} is disabled, publishing an + * ISO19115-3.2018 record must leave the stored XML untouched (no publication date added). + */ + @Test + public void publishIso191153DoesNotUpdatePublicationDateWhenManageDisabled() throws Exception { + settingManager.setValue(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE, false); + settingManager.setValue(Settings.METADATA_WORKFLOW_ALLOW_PUBLISH_INVALID_MD, true); + + int isoId = injectIso191153Record(); + String isoUuid = metadataRepository.findById(isoId).get().getUuid(); + + List ns = new ArrayList<>(ISO19115_3_2018SchemaPlugin.allNamespaces); + + Element beforeXml = metadataRepository.findById(isoId).get().getXmlData(false); + int dateInfoCountBefore = Xml.selectNodes(beforeXml, "mdb:dateInfo", ns).size(); + assertEquals(0, Xml.selectNodes(beforeXml, PUBLICATION_DATE_INFO_XPATH, ns).size()); + + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + MockHttpSession mockHttpSession = loginAs(reviewerUser); + mockMvc.perform(put("/srv/api/records/" + isoUuid + "/publish") + .session(mockHttpSession)) + .andExpect(status().isNoContent()); + + // The record is published ... + List ops = operationAllowedRepository.findAllById_MetadataId(isoId); + assertTrue("Record should be published", + ops.stream().anyMatch(op -> op.getId().getGroupId() == ReservedGroup.all.getId())); + + // ... but its XML must not gain a publication date, and its dateInfo blocks are unchanged. + entityManager.flush(); + entityManager.clear(); + Element afterXml = metadataRepository.findById(isoId).get().getXmlData(false); + + assertEquals("No publication date should be added when the setting is disabled", + 0, Xml.selectNodes(afterXml, PUBLICATION_DATE_INFO_XPATH, ns).size()); + assertEquals("The metadata dateInfo blocks should be unchanged when the setting is disabled", + dateInfoCountBefore, Xml.selectNodes(afterXml, "mdb:dateInfo", ns).size()); + } + + /** + * Injects an ISO19115-3.2018 sample record owned by {@code editorUser} in the sample group so that + * {@code reviewerUser} (a Reviewer in that group) can publish it. + */ + private int injectIso191153Record() throws Exception { + Metadata md = (Metadata) injectMetadataInDb(getSampleISO19115MetadataXml(), context); + md.getSourceInfo().setOwner(editorUser.getId()); + md.getSourceInfo().setGroupOwner(SAMPLE_GROUP_ID); + metadataRepository.save(md); + return md.getId(); + } + /** * Creates a new workspace group with the given name, saves it, and adds a * {@code UserGroup(profile=UserAdmin)} entry for {@code user} in that group. diff --git a/services/src/test/java/org/fao/geonet/api/records/events/MetadataRemovePublicationDateListenerTest.java b/services/src/test/java/org/fao/geonet/api/records/events/MetadataRemovePublicationDateListenerTest.java new file mode 100644 index 000000000000..a93a8d1ba50b --- /dev/null +++ b/services/src/test/java/org/fao/geonet/api/records/events/MetadataRemovePublicationDateListenerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2001-2026 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.records.events; + +import org.fao.geonet.domain.Metadata; +import org.fao.geonet.domain.MetadataDataInfo; +import org.fao.geonet.domain.MetadataDraft; +import org.fao.geonet.domain.MetadataType; +import org.fao.geonet.events.md.MetadataUnpublished; +import org.fao.geonet.kernel.datamanager.IMetadataUtils; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MetadataRemovePublicationDateListenerTest { + + private MetadataRemovePublicationDateListener listener; + private MetadataUpdatePublicationDateService service; + private SettingManager settingManager; + private IMetadataUtils metadataUtils; + + @Before + public void setUp() throws Exception { + listener = new MetadataRemovePublicationDateListener(); + service = mock(MetadataUpdatePublicationDateService.class); + settingManager = mock(SettingManager.class); + metadataUtils = mock(IMetadataUtils.class); + + setField("metadataUpdatePublicationDateService", service); + setField("settingManager", settingManager); + setField("metadataUtils", metadataUtils); + } + + private void setField(String name, Object value) throws Exception { + Field field = MetadataRemovePublicationDateListener.class.getDeclaredField(name); + field.setAccessible(true); + field.set(listener, value); + } + + private Metadata metadataWithType(MetadataType type, int id, String uuid) { + Metadata md = mock(Metadata.class); + MetadataDataInfo dataInfo = mock(MetadataDataInfo.class); + when(dataInfo.getType()).thenReturn(type); + when(md.getDataInfo()).thenReturn(dataInfo); + when(md.getId()).thenReturn(id); + when(md.getUuid()).thenReturn(uuid); + return md; + } + + @Test + public void doesNothingWhenSettingDisabled() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(false); + + Metadata md = metadataWithType(MetadataType.METADATA, 1, "uuid-1"); + listener.onApplicationEvent(new MetadataUnpublished(md)); + + verify(service, never()).removePublicationDate(any()); + } + + @Test + public void doesNothingForTemplates() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + Metadata md = metadataWithType(MetadataType.TEMPLATE, 1, "uuid-1"); + listener.onApplicationEvent(new MetadataUnpublished(md)); + + verify(service, never()).removePublicationDate(any()); + } + + @Test + public void removesPublicationDateForNormalMetadata() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + Metadata md = metadataWithType(MetadataType.METADATA, 1, "uuid-1"); + listener.onApplicationEvent(new MetadataUnpublished(md)); + + verify(service).removePublicationDate(eq(md)); + } + + @Test + public void resolvesApprovedRecordWhenEventCarriesADraft() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + MetadataDraft draft = mock(MetadataDraft.class); + when(draft.getUuid()).thenReturn("uuid-1"); + + Metadata approved = metadataWithType(MetadataType.METADATA, 42, "uuid-1"); + when(metadataUtils.findOneByUuid("uuid-1")).thenReturn(approved); + + listener.onApplicationEvent(new MetadataUnpublished(draft)); + + // The approved record must be used, not the draft itself. + verify(service).removePublicationDate(eq(approved)); + } + + @Test + public void doesNothingWhenDraftCannotBeResolved() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + MetadataDraft draft = mock(MetadataDraft.class); + when(draft.getUuid()).thenReturn("uuid-1"); + when(metadataUtils.findOneByUuid("uuid-1")).thenReturn(null); + + listener.onApplicationEvent(new MetadataUnpublished(draft)); + + verify(service, never()).removePublicationDate(any()); + } + + @Test + public void serviceFailureDoesNotPropagate() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + Metadata md = metadataWithType(MetadataType.METADATA, 1, "uuid-1"); + org.mockito.Mockito.doThrow(new RuntimeException("boom")).when(service).removePublicationDate(any()); + + // Must not throw: a failure removing the publication date must not break the unpublish action. + listener.onApplicationEvent(new MetadataUnpublished(md)); + } +} diff --git a/services/src/test/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateListenerTest.java b/services/src/test/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateListenerTest.java new file mode 100644 index 000000000000..af20570032cd --- /dev/null +++ b/services/src/test/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateListenerTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2001-2026 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.records.events; + +import org.fao.geonet.domain.ISODate; +import org.fao.geonet.domain.Metadata; +import org.fao.geonet.domain.MetadataDataInfo; +import org.fao.geonet.domain.MetadataDraft; +import org.fao.geonet.domain.MetadataType; +import org.fao.geonet.events.md.MetadataPublished; +import org.fao.geonet.kernel.datamanager.IMetadataUtils; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MetadataUpdatePublicationDateListenerTest { + + private MetadataUpdatePublicationDateListener listener; + private MetadataUpdatePublicationDateService service; + private SettingManager settingManager; + private IMetadataUtils metadataUtils; + + @Before + public void setUp() throws Exception { + listener = new MetadataUpdatePublicationDateListener(); + service = mock(MetadataUpdatePublicationDateService.class); + settingManager = mock(SettingManager.class); + metadataUtils = mock(IMetadataUtils.class); + + setField("metadataUpdatePublicationDateService", service); + setField("settingManager", settingManager); + setField("metadataUtils", metadataUtils); + } + + private void setField(String name, Object value) throws Exception { + Field field = MetadataUpdatePublicationDateListener.class.getDeclaredField(name); + field.setAccessible(true); + field.set(listener, value); + } + + private Metadata metadataWithType(MetadataType type, int id, String uuid) { + Metadata md = mock(Metadata.class); + MetadataDataInfo dataInfo = mock(MetadataDataInfo.class); + when(dataInfo.getType()).thenReturn(type); + when(md.getDataInfo()).thenReturn(dataInfo); + when(md.getId()).thenReturn(id); + when(md.getUuid()).thenReturn(uuid); + return md; + } + + @Test + public void doesNothingWhenSettingDisabled() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(false); + + Metadata md = metadataWithType(MetadataType.METADATA, 1, "uuid-1"); + listener.onApplicationEvent(new MetadataPublished(md)); + + verify(service, never()).addPublicationDate(any(), any()); + } + + @Test + public void doesNothingForTemplates() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + Metadata md = metadataWithType(MetadataType.TEMPLATE, 1, "uuid-1"); + listener.onApplicationEvent(new MetadataPublished(md)); + + verify(service, never()).addPublicationDate(any(), any()); + } + + @Test + public void setsPublicationDateForNormalMetadata() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + Metadata md = metadataWithType(MetadataType.METADATA, 1, "uuid-1"); + listener.onApplicationEvent(new MetadataPublished(md)); + + verify(service).addPublicationDate(eq(md), any(ISODate.class)); + } + + @Test + public void resolvesApprovedRecordWhenEventCarriesADraft() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + MetadataDraft draft = mock(MetadataDraft.class); + when(draft.getUuid()).thenReturn("uuid-1"); + + Metadata approved = metadataWithType(MetadataType.METADATA, 42, "uuid-1"); + when(metadataUtils.findOneByUuid("uuid-1")).thenReturn(approved); + + listener.onApplicationEvent(new MetadataPublished(draft)); + + // The approved record must be used, not the draft itself. + verify(service).addPublicationDate(eq(approved), any(ISODate.class)); + } + + @Test + public void doesNothingWhenDraftCannotBeResolved() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + MetadataDraft draft = mock(MetadataDraft.class); + when(draft.getUuid()).thenReturn("uuid-1"); + when(metadataUtils.findOneByUuid("uuid-1")).thenReturn(null); + + listener.onApplicationEvent(new MetadataPublished(draft)); + + verify(service, never()).addPublicationDate(any(), any()); + } + + @Test + public void serviceFailureDoesNotPropagate() { + when(settingManager.getValueAsBool(Settings.SYSTEM_METADATAPRIVS_PUBLICATION_MANAGEPUBLICATIONDATE)).thenReturn(true); + + Metadata md = metadataWithType(MetadataType.METADATA, 1, "uuid-1"); + org.mockito.Mockito.doThrow(new RuntimeException("boom")).when(service).addPublicationDate(any(), any()); + + // Must not throw: a failure updating the publication date must not break the publish action. + listener.onApplicationEvent(new MetadataPublished(md)); + } +} diff --git a/services/src/test/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateServiceIntegrationTest.java b/services/src/test/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateServiceIntegrationTest.java new file mode 100644 index 000000000000..bd96f23f1871 --- /dev/null +++ b/services/src/test/java/org/fao/geonet/api/records/events/MetadataUpdatePublicationDateServiceIntegrationTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2001-2026 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.records.events; + +import jeeves.server.context.ServiceContext; +import org.fao.geonet.domain.AbstractMetadata; +import org.fao.geonet.domain.ISODate; +import org.fao.geonet.kernel.datamanager.IMetadataUtils; +import org.fao.geonet.kernel.search.IndexingMode; +import org.fao.geonet.schema.iso19115_3_2018.ISO19115_3_2018SchemaPlugin; +import org.fao.geonet.services.AbstractServiceIntegrationTest; +import org.fao.geonet.utils.Xml; +import org.jdom.Element; +import org.jdom.Namespace; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class MetadataUpdatePublicationDateServiceIntegrationTest extends AbstractServiceIntegrationTest { + + @Autowired + private MetadataUpdatePublicationDateService service; + + @Autowired + private IMetadataUtils repository; + + @PersistenceContext + private EntityManager entityManager; + + private ServiceContext context; + private AbstractMetadata metadata; + private List ns = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + context = createServiceContext(); + loginAsAdmin(context); + + ns.addAll(ISO19115_3_2018SchemaPlugin.allNamespaces); + + Element sample = getSampleISO19115MetadataXml(); + metadata = injectMetadataInDb(sample, context, IndexingMode.full); + } + + @Test + public void testAddPublicationDate() throws Exception { + AbstractMetadata metadataRecord = repository.findOne(metadata.getId()); + Element initialXml = metadataRecord.getXmlData(false); + + // Ensure no publication date exists + assertEquals(0, Xml.selectNodes(initialXml, "mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']", ns).size()); + + ISODate pubDate = new ISODate("2026-07-02"); + service.addPublicationDate(metadataRecord, pubDate); + entityManager.flush(); + entityManager.clear(); + + AbstractMetadata updatedRecord = repository.findOne(metadata.getId()); + Element updatedXml = updatedRecord.getXmlData(false); + + assertEquals(1, Xml.selectNodes(updatedXml, "mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']", ns).size()); + Element dateText = (Element) Xml.selectSingle(updatedXml, "mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']/cit:CI_Date/cit:date/gco:Date", ns); + assertNotNull(dateText); + assertEquals(pubDate.toString(), dateText.getText()); + } + + @Test + public void testRemovePublicationDate() throws Exception { + AbstractMetadata metadataRecord = repository.findOne(metadata.getId()); + + // 1. Add a publication date first + ISODate pubDate = new ISODate("2026-07-02"); + service.addPublicationDate(metadataRecord, pubDate); + entityManager.flush(); + entityManager.clear(); + + AbstractMetadata recordWithPubDate = repository.findOne(metadata.getId()); + assertEquals(1, Xml.selectNodes(recordWithPubDate.getXmlData(false), "mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']", ns).size()); + + // 2. Remove it + service.removePublicationDate(recordWithPubDate); + entityManager.flush(); + entityManager.clear(); + + AbstractMetadata updatedRecord = repository.findOne(metadata.getId()); + Element updatedXml = updatedRecord.getXmlData(false); + + assertEquals(0, Xml.selectNodes(updatedXml, "mdb:dateInfo[cit:CI_Date/cit:dateType/cit:CI_DateTypeCode/@codeListValue = 'publication']", ns).size()); + } +} diff --git a/web-ui/src/main/resources/catalog/locales/en-admin.json b/web-ui/src/main/resources/catalog/locales/en-admin.json index 67bf99e235d4..01378978b4e5 100644 --- a/web-ui/src/main/resources/catalog/locales/en-admin.json +++ b/web-ui/src/main/resources/catalog/locales/en-admin.json @@ -796,6 +796,8 @@ "system/metadataprivs/usergrouponly": "Only set privileges to user's groups", "system/metadataprivs/publicationbyrevieweringroupowneronly": "Publication by users reviewer in record group only", "system/metadataprivs/publicationbyrevieweringroupowneronly-help": "Allow publication by administrator and reviewer member of record group. If false, then also all users reviewer in group with editing rights can publish/unpublish a record.", + "system/metadataprivs/publication/managepublicationdate": "Manage the publication date automatically (only applies to ISO19115-3)", + "system/metadataprivs/publication/managepublicationdate-help": "When enabled the publication date of the ISO19115-3 metadata is set automatically when the metadata is published and removed when the metadata is unpublished", "system/metadataprivs/publication/notificationLevel": "Notification level when a metadata is published / unpublished", "system/metadataprivs/publication/notificationLevel-help": "Define which users to alert when a metadata is published / unpublished", "system/metadataprivs/publication/notificationGroups": "Groups to notify when a metadata is published / unpublished", diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/metadata.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/metadata.html index 92c79a4dd8a2..e1bf98a75af6 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/recordView/metadata.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/metadata.html @@ -9,6 +9,19 @@

updatedOn

> +
+ + + +
+

publicationDate

+

{{mdView.current.record.publicationDate}}

+
+
+