From 1fd8b526a785e695b56f6a6d062709e4619b1aae Mon Sep 17 00:00:00 2001
From: tylerjmchugh <163562062+tylerjmchugh@users.noreply.github.com>
Date: Wed, 19 Jun 2024 12:14:21 -0400
Subject: [PATCH] Multilingual Emails (#8044)
* Add settings fields for localized emails
* Add spring bean to initialize feedback email locales from settings field
* Modify settings manager to update feedback locales when the settings fields are saved
* Create classes for localized emails, their components, and the components' parameters
* Implement lang in getIndexField
* Localize workflow status emails
* Localize metadata publication emails
* Localize user feedback emails
* Localize RegisterApi emails
* Localize PasswordApi emails
* Localize WatchListNotifier emails
* Localize MailApi emails
* Update migration script to only insert settings fields if not present
* Add static enum imports for readability
* Log a warning when a locale is invalid or missing
* Update log modules and messages
* Trim language codes defined in settings to handle spaces after commas
* Rename translation follows label to translation follows text for consistency
* Add back resource bundle 'messages' that was unused before merging main
* Add logic to break from loop when email subject and text messages fail
---
.../fao/geonet/kernel/WatchListNotifier.java | 88 +++--
.../kernel/metadata/DefaultStatusActions.java | 134 +++++--
.../geonet/kernel/setting/SettingManager.java | 10 +
.../fao/geonet/kernel/setting/Settings.java | 2 +
.../geonet/languages/FeedbackLanguages.java | 129 ++++++
.../org/fao/geonet/util/LocalizedEmail.java | 149 +++++++
.../geonet/util/LocalizedEmailComponent.java | 372 ++++++++++++++++++
.../geonet/util/LocalizedEmailParameter.java | 179 +++++++++
.../resources/config-spring-geonetwork.xml | 2 +
.../org/fao/geonet/api/Messages.properties | 6 +-
.../fao/geonet/api/Messages_fre.properties | 10 +
.../api/records/MetadataSharingApi.java | 15 +-
.../api/records/MetadataWorkflowApi.java | 11 +-
.../fao/geonet/api/tools/mail/MailApi.java | 46 ++-
.../api/userfeedback/UserFeedbackAPI.java | 61 ++-
.../org/fao/geonet/api/users/PasswordApi.java | 81 +++-
.../org/fao/geonet/api/users/RegisterApi.java | 141 ++++---
.../util/MetadataPublicationMailNotifier.java | 102 ++---
.../userfeedback/partials/mdFeedback.html | 34 +-
.../resources/catalog/locales/en-admin.json | 4 +
.../org/fao/geonet/api/Messages.properties | 6 +-
.../fao/geonet/api/Messages_fre.properties | 10 +
.../setup/sql/data/data-db-default.sql | 2 +
.../sql/migrate/v445/migrate-default.sql | 3 +
.../domain-repository-test-context.xml | 2 +-
25 files changed, 1355 insertions(+), 244 deletions(-)
create mode 100644 core/src/main/java/org/fao/geonet/languages/FeedbackLanguages.java
create mode 100644 core/src/main/java/org/fao/geonet/util/LocalizedEmail.java
create mode 100644 core/src/main/java/org/fao/geonet/util/LocalizedEmailComponent.java
create mode 100644 core/src/main/java/org/fao/geonet/util/LocalizedEmailParameter.java
diff --git a/core/src/main/java/org/fao/geonet/kernel/WatchListNotifier.java b/core/src/main/java/org/fao/geonet/kernel/WatchListNotifier.java
index 7291dd8ff8b..09a17638f1a 100644
--- a/core/src/main/java/org/fao/geonet/kernel/WatchListNotifier.java
+++ b/core/src/main/java/org/fao/geonet/kernel/WatchListNotifier.java
@@ -30,9 +30,13 @@
import org.fao.geonet.domain.Selection;
import org.fao.geonet.domain.User;
import org.fao.geonet.kernel.setting.SettingManager;
+import org.fao.geonet.languages.FeedbackLanguages;
import org.fao.geonet.repository.SelectionRepository;
import org.fao.geonet.repository.UserRepository;
import org.fao.geonet.repository.UserSavedSelectionRepository;
+import org.fao.geonet.util.LocalizedEmail;
+import org.fao.geonet.util.LocalizedEmailParameter;
+import org.fao.geonet.util.LocalizedEmailComponent;
import org.fao.geonet.util.MailUtil;
import org.fao.geonet.utils.Log;
import org.quartz.JobExecutionContext;
@@ -44,6 +48,10 @@
import java.util.*;
import static org.fao.geonet.kernel.setting.Settings.SYSTEM_USER_LASTNOTIFICATIONDATE;
+import static org.fao.geonet.util.LocalizedEmailComponent.ComponentType.*;
+import static org.fao.geonet.util.LocalizedEmailComponent.KeyType;
+import static org.fao.geonet.util.LocalizedEmailComponent.ReplacementType.*;
+import static org.fao.geonet.util.LocalizedEmailParameter.ParameterType;
/**
* Task checking on a regular basis the list of records
@@ -53,15 +61,13 @@ public class WatchListNotifier extends QuartzJobBean {
private String lastNotificationDate;
private String nextLastNotificationDate;
- private String subject;
- private String message;
- private String recordMessage;
private String updatedRecordPermalink;
private String language = "eng";
private SettingManager settingManager;
private ApplicationContext appContext;
private UserSavedSelectionRepository userSavedSelectionRepository;
private UserRepository userRepository;
+ private FeedbackLanguages feedbackLanguages;
@Value("${usersavedselection.watchlist.searchurl}")
private String permalinkApp = "catalog.search#/search?_uuid={{filter}}";
@@ -92,20 +98,7 @@ public WatchListNotifier() {
protected void executeInternal(JobExecutionContext jobContext) throws JobExecutionException {
appContext = ApplicationContextHolder.get();
settingManager = appContext.getBean(SettingManager.class);
-
- ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages",
- new Locale(
- language
- ));
-
- try {
- subject = messages.getString("user_watchlist_subject");
- message = messages.getString("user_watchlist_message");
- recordMessage = messages.getString("user_watchlist_message_record").
- replace("{{link}}",
- settingManager.getNodeURL() + permalinkRecordApp);
- } catch (Exception e) {
- }
+ feedbackLanguages = appContext.getBean(FeedbackLanguages.class);
updatedRecordPermalink = settingManager.getSiteURL(language);
@@ -166,6 +159,9 @@ protected void executeInternal(JobExecutionContext jobContext) throws JobExecuti
}
private void notify(Integer selectionId, Integer userId) {
+
+ Locale[] feedbackLocales = feedbackLanguages.getLocales(new Locale(language));
+
// Get metadata with changes since last notification
// TODO: Could be relevant to get versionning system info once available
// and report deleted records too.
@@ -188,27 +184,51 @@ private void notify(Integer selectionId, Integer userId) {
// TODO: We should send email depending on user language
Optional user = userRepository.findById(userId);
if (user.isPresent() && StringUtils.isNotEmpty(user.get().getEmail())) {
+ String url = updatedRecordPermalink +
+ permalinkApp.replace("{{filter}}", String.join(" or ", updatedRecords));
- // Build message
- StringBuffer listOfUpdateMessage = new StringBuffer();
- for (String record : updatedRecords) {
- try {
- listOfUpdateMessage.append(
- MailUtil.compileMessageWithIndexFields(recordMessage, record, this.language)
- );
- } catch (Exception e) {
- Log.error(Geonet.USER_WATCHLIST, e.getMessage(), e);
+ LocalizedEmailComponent emailSubjectComponent = new LocalizedEmailComponent(SUBJECT, "user_watchlist_subject", KeyType.MESSAGE_KEY, POSITIONAL_FORMAT);
+ LocalizedEmailComponent emailMessageComponent = new LocalizedEmailComponent(MESSAGE, "user_watchlist_message", KeyType.MESSAGE_KEY, POSITIONAL_FORMAT);
+
+ for (Locale feedbackLocale : feedbackLocales) {
+
+ // Build message
+ StringBuffer listOfUpdateMessage = new StringBuffer();
+ for (String record : updatedRecords) {
+ LocalizedEmailComponent recordMessageComponent = new LocalizedEmailComponent(NESTED, "user_watchlist_message_record", KeyType.MESSAGE_KEY, NAMED_FORMAT);
+ recordMessageComponent.enableCompileWithIndexFields(record);
+ recordMessageComponent.enableReplaceLinks(true);
+ try {
+ listOfUpdateMessage.append(
+ recordMessageComponent.parseMessage(feedbackLocale)
+ );
+ } catch (Exception e) {
+ Log.error(Geonet.USER_WATCHLIST, e.getMessage(), e);
+ }
}
+
+ emailSubjectComponent.addParameters(
+ feedbackLocale,
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, settingManager.getSiteName()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, updatedRecords.size()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, lastNotificationDate)
+ );
+
+ emailMessageComponent.addParameters(
+ feedbackLocale,
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, listOfUpdateMessage.toString()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, lastNotificationDate),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, url),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 4, url)
+ );
+
}
- String url = updatedRecordPermalink +
- permalinkApp.replace("{{filter}}", String.join(" or ", updatedRecords));
- String mailSubject = String.format(subject,
- settingManager.getSiteName(), updatedRecords.size(), lastNotificationDate);
- String htmlMessage = String.format(message,
- listOfUpdateMessage.toString(),
- lastNotificationDate,
- url, url);
+ LocalizedEmail localizedEmail = new LocalizedEmail(true);
+ localizedEmail.addComponents(emailSubjectComponent, emailMessageComponent);
+
+ String mailSubject = localizedEmail.getParsedSubject(feedbackLocales);
+ String htmlMessage = localizedEmail.getParsedMessage(feedbackLocales);
if (Log.isDebugEnabled(Geonet.USER_WATCHLIST)) {
Log.debug(Geonet.USER_WATCHLIST, String.format(
diff --git a/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java b/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java
index 983b9e44d94..cdb7a8bf8f7 100644
--- a/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java
+++ b/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java
@@ -38,15 +38,22 @@
import org.fao.geonet.kernel.setting.Settings;
import org.fao.geonet.repository.*;
import org.fao.geonet.repository.specification.GroupSpecs;
+import org.fao.geonet.util.LocalizedEmail;
+import org.fao.geonet.util.LocalizedEmailParameter;
+import org.fao.geonet.util.LocalizedEmailComponent;
+import org.fao.geonet.languages.FeedbackLanguages;
import org.fao.geonet.util.MailUtil;
import org.fao.geonet.utils.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
-import java.text.MessageFormat;
import java.util.*;
import static org.fao.geonet.kernel.setting.Settings.SYSTEM_FEEDBACK_EMAIL;
+import static org.fao.geonet.util.LocalizedEmailComponent.ComponentType.*;
+import static org.fao.geonet.util.LocalizedEmailComponent.KeyType;
+import static org.fao.geonet.util.LocalizedEmailComponent.ReplacementType.*;
+import static org.fao.geonet.util.LocalizedEmailParameter.ParameterType;
public class DefaultStatusActions implements StatusActions {
@@ -240,61 +247,106 @@ protected void notify(List userToNotify, MetadataStatus status) throws Exc
return;
}
- ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", new Locale(this.language));
+ ApplicationContext applicationContext = ApplicationContextHolder.get();
+ FeedbackLanguages feedbackLanguages = applicationContext.getBean(FeedbackLanguages.class);
- String translatedStatusName = getTranslatedStatusName(status.getStatusValue().getId());
- // TODO: Refactor to allow custom messages based on the type of status
- String subjectTemplate = "";
- try {
- subjectTemplate = messages
- .getString("status_change_" + status.getStatusValue().getName() + "_email_subject");
- } catch (MissingResourceException e) {
- subjectTemplate = messages.getString("status_change_default_email_subject");
- }
- String subject = MessageFormat.format(subjectTemplate, siteName, translatedStatusName, replyToDescr // Author of the change
- );
+ Locale[] feedbackLocales = feedbackLanguages.getLocales(new Locale(this.language));
Set listOfId = new HashSet<>(1);
listOfId.add(status.getMetadataId());
- String textTemplate = "";
- try {
- textTemplate = messages.getString("status_change_" + status.getStatusValue().getName() + "_email_text");
- } catch (MissingResourceException e) {
- textTemplate = messages.getString("status_change_default_email_text");
- }
-
- // Replace link in message
- ApplicationContext applicationContext = ApplicationContextHolder.get();
- SettingManager sm = applicationContext.getBean(SettingManager.class);
- textTemplate = textTemplate.replace("{{link}}", sm.getNodeURL()+ "api/records/'{{'index:uuid'}}'");
-
UserRepository userRepository = context.getBean(UserRepository.class);
User owner = userRepository.findById(status.getOwner()).orElse(null);
IMetadataUtils metadataRepository = ApplicationContextHolder.get().getBean(IMetadataUtils.class);
AbstractMetadata metadata = metadataRepository.findOne(status.getMetadataId());
- String metadataUrl = metadataUtils.getDefaultUrl(metadata.getUuid(), this.language);
+ String subjectTemplateKey = "";
+ String textTemplateKey = "";
+ boolean failedToFindASpecificSubjectTemplate = false;
+ boolean failedToFindASpecificTextTemplate = false;
+
+ for (Locale feedbackLocale: feedbackLocales) {
+ ResourceBundle resourceBundle = ResourceBundle.getBundle("org.fao.geonet.api.Messages", feedbackLocale);
+
+ if (!failedToFindASpecificSubjectTemplate) {
+ try {
+ subjectTemplateKey = "status_change_" + status.getStatusValue().getName() + "_email_subject";
+ resourceBundle.getString(subjectTemplateKey);
+ } catch (MissingResourceException e) {
+ failedToFindASpecificSubjectTemplate = true;
+ }
+ }
+
+ if (!failedToFindASpecificTextTemplate) {
+ try {
+ textTemplateKey = "status_change_" + status.getStatusValue().getName() + "_email_text";
+ resourceBundle.getString(textTemplateKey);
+ } catch (MissingResourceException e) {
+ failedToFindASpecificTextTemplate = true;
+ }
+ }
+
+ if ((failedToFindASpecificSubjectTemplate) && (failedToFindASpecificTextTemplate)) break;
+ }
+
+ if (failedToFindASpecificSubjectTemplate) {
+ subjectTemplateKey = "status_change_default_email_subject";
+ }
+
+ if (failedToFindASpecificTextTemplate) {
+ textTemplateKey = "status_change_default_email_text";
+ }
+
+ LocalizedEmailComponent emailSubjectComponent = new LocalizedEmailComponent(SUBJECT, subjectTemplateKey, KeyType.MESSAGE_KEY, NUMERIC_FORMAT);
+ emailSubjectComponent.enableCompileWithIndexFields(metadata.getUuid());
+
+ LocalizedEmailComponent emailMessageComponent = new LocalizedEmailComponent(MESSAGE, textTemplateKey, KeyType.MESSAGE_KEY, NUMERIC_FORMAT);
+ emailMessageComponent.enableCompileWithIndexFields(metadata.getUuid());
+ emailMessageComponent.enableReplaceLinks(false);
+
+ LocalizedEmailComponent emailSalutationComponent = new LocalizedEmailComponent(SALUTATION, "{{userName}},\n\n", KeyType.RAW_VALUE, NONE);
+
+ for (Locale feedbackLocale : feedbackLocales) {
+ // TODO: Refactor to allow custom messages based on the type of status
+
+ emailSubjectComponent.addParameters(
+ feedbackLocale,
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, siteName),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, getTranslatedStatusName(status.getStatusValue().getId(), feedbackLocale)),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, replyToDescr)
+ );
+
+ emailMessageComponent.addParameters(
+ feedbackLocale,
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, replyToDescr),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, status.getChangeMessage()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, getTranslatedStatusName(status.getStatusValue().getId(), feedbackLocale)),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 4, status.getChangeDate()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 5, status.getDueDate()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 6, status.getCloseDate()),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 7, owner == null ? "" : Joiner.on(" ").skipNulls().join(owner.getName(), owner.getSurname())),
+ new LocalizedEmailParameter(ParameterType.RAW_VALUE, 8, metadataUtils.getDefaultUrl(metadata.getUuid(), feedbackLocale.getISO3Language()))
+ );
+ }
- String message = MessageFormat.format(textTemplate, replyToDescr, // Author of the change
- status.getChangeMessage(), translatedStatusName, status.getChangeDate(), status.getDueDate(),
- status.getCloseDate(),
- owner == null ? "" : Joiner.on(" ").skipNulls().join(owner.getName(), owner.getSurname()),
- metadataUrl);
+ LocalizedEmail localizedEmail = new LocalizedEmail(false);
+ localizedEmail.addComponents(emailSubjectComponent, emailMessageComponent, emailSalutationComponent);
+ String subject = localizedEmail.getParsedSubject(feedbackLocales);
- subject = MailUtil.compileMessageWithIndexFields(subject, metadata.getUuid(), this.language);
- message = MailUtil.compileMessageWithIndexFields(message, metadata.getUuid(), this.language);
for (User user : userToNotify) {
- String salutation = Joiner.on(" ").skipNulls().join(user.getName(), user.getSurname());
- //If we have a salutation then end it with a ","
- if (StringUtils.isEmpty(salutation)) {
- salutation = "";
+ String userName = Joiner.on(" ").skipNulls().join(user.getName(), user.getSurname());
+ //If we have a userName add the salutation
+ String message;
+ if (StringUtils.isEmpty(userName)) {
+ message = localizedEmail.getParsedMessage(feedbackLocales);
} else {
- salutation += ",\n\n";
+ Map replacements = new HashMap<>();
+ replacements.put("{{userName}}", userName);
+ message = localizedEmail.getParsedMessage(feedbackLocales, replacements);
}
- sendEmail(user.getEmail(), subject, salutation + message);
+ sendEmail(user.getEmail(), subject, message);
}
}
@@ -408,14 +460,14 @@ protected void unsetAllOperations(int mdId) throws Exception {
}
}
- private String getTranslatedStatusName(int statusValueId) {
+ private String getTranslatedStatusName(int statusValueId, Locale locale) {
String translatedStatusName = "";
StatusValue s = statusValueRepository.findOneById(statusValueId);
if (s == null) {
translatedStatusName = statusValueId
+ " (Status not found in database translation table. Check the content of the StatusValueDes table.)";
} else {
- translatedStatusName = s.getLabel(this.language);
+ translatedStatusName = s.getLabel(locale.getISO3Language());
}
return translatedStatusName;
}
diff --git a/core/src/main/java/org/fao/geonet/kernel/setting/SettingManager.java b/core/src/main/java/org/fao/geonet/kernel/setting/SettingManager.java
index a3cd94bcb3c..b6f015d6b58 100644
--- a/core/src/main/java/org/fao/geonet/kernel/setting/SettingManager.java
+++ b/core/src/main/java/org/fao/geonet/kernel/setting/SettingManager.java
@@ -33,6 +33,7 @@
import org.fao.geonet.domain.Setting;
import org.fao.geonet.domain.SettingDataType;
import org.fao.geonet.domain.Setting_;
+import org.fao.geonet.languages.FeedbackLanguages;
import org.fao.geonet.repository.SettingRepository;
import org.fao.geonet.repository.SortUtils;
import org.fao.geonet.repository.SourceRepository;
@@ -94,6 +95,9 @@ public class SettingManager {
@Autowired
DefaultLanguage defaultLanguage;
+ @Autowired
+ FeedbackLanguages feedbackLanguages;
+
@PostConstruct
private void init() {
this.pathFinder = new ServletPathFinder(servletContext);
@@ -343,6 +347,12 @@ public boolean setValue(String key, String value) {
repo.save(setting);
+ if (key.equals("system/feedback/languages")) {
+ feedbackLanguages.updateSupportedLocales();
+ } else if (key.equals("system/feedback/translationFollowsText")) {
+ feedbackLanguages.updateTranslationFollowsText();
+ }
+
return true;
}
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 6b30d61e810..a96fa132585 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
@@ -60,6 +60,8 @@ public class Settings {
public static final String SYSTEM_USERS_IDENTICON = "system/users/identicon";
public static final String SYSTEM_SEARCHSTATS = "system/searchStats/enable";
public static final String SYSTEM_FEEDBACK_EMAIL = "system/feedback/email";
+ public static final String SYSTEM_FEEDBACK_LANGUAGES = "system/feedback/languages";
+ public static final String SYSTEM_FEEDBACK_TRANSLATION_FOLLOWS_TEXT = "system/feedback/translationFollowsText";
public static final String SYSTEM_FEEDBACK_MAILSERVER_HOST = "system/feedback/mailServer/host";
public static final String SYSTEM_FEEDBACK_MAILSERVER_PORT = "system/feedback/mailServer/port";
public static final String SYSTEM_FEEDBACK_MAILSERVER_USERNAME = "system/feedback/mailServer/username";
diff --git a/core/src/main/java/org/fao/geonet/languages/FeedbackLanguages.java b/core/src/main/java/org/fao/geonet/languages/FeedbackLanguages.java
new file mode 100644
index 00000000000..183ac8426f5
--- /dev/null
+++ b/core/src/main/java/org/fao/geonet/languages/FeedbackLanguages.java
@@ -0,0 +1,129 @@
+//=============================================================================
+//=== 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 library is free software; you can redistribute it and/or
+//=== modify it under the terms of the GNU Lesser General Public
+//=== License as published by the Free Software Foundation; either
+//=== version 2.1 of the License, or (at your option) any later version.
+//===
+//=== This library 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
+//=== Lesser General Public License for more details.
+//===
+//=== You should have received a copy of the GNU Lesser General Public
+//=== License along with this library; 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.languages;
+
+import org.apache.commons.lang.StringUtils;
+import org.fao.geonet.constants.Geonet;
+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 javax.annotation.PostConstruct;
+import java.util.*;
+
+/**
+ * Represents a utility class for managing supported locales and translation follows text for feedback.
+ */
+public class FeedbackLanguages {
+ private Locale[] supportedLocales;
+ private String translationFollowsText;
+
+ @Autowired
+ SettingManager settingManager;
+
+ /**
+ * Initializes the supported locales and translation follows text after bean creation.
+ */
+ @PostConstruct
+ public void init() {
+ updateSupportedLocales();
+ updateTranslationFollowsText();
+ }
+
+ /**
+ * Updates the supported locales based on the system feedback languages setting.
+ */
+ public void updateSupportedLocales() {
+ String systemFeedbackLanguages = getSettingsValue(Settings.SYSTEM_FEEDBACK_LANGUAGES);
+
+ if (StringUtils.isBlank(systemFeedbackLanguages)) {
+ supportedLocales = null;
+ return;
+ }
+
+ supportedLocales = Arrays.stream(systemFeedbackLanguages.split(","))
+ .map(String::trim)
+ .map(Locale::new)
+ .filter(this::isValidLocale)
+ .toArray(Locale[]::new);
+ }
+
+ /**
+ * Updates the translation follows text based on the system feedback translation text setting.
+ */
+ public void updateTranslationFollowsText() {
+ translationFollowsText = getSettingsValue(Settings.SYSTEM_FEEDBACK_TRANSLATION_FOLLOWS_TEXT);
+ }
+
+ /**
+ * Retrieves the supported locales. If no supported locales are found, returns a fallback locale.
+ * @param fallbackLocale The fallback locale to be returned if no supported locales are available.
+ * @return An array of supported locales or a single fallback locale if none are available.
+ */
+ public Locale[] getLocales(Locale fallbackLocale) {
+ if (supportedLocales == null || supportedLocales.length < 1) {
+ return new Locale[] { fallbackLocale };
+ }
+
+ return supportedLocales;
+ }
+
+ /**
+ * Retrieves the translation follows text.
+ * @return The translation follows text.
+ */
+ public String getTranslationFollowsText() {
+ return translationFollowsText;
+ }
+
+ /**
+ * Checks if the provided locale is valid by attempting to load a ResourceBundle.
+ * @param locale The locale to validate.
+ * @return True if the locale is valid, false otherwise.
+ */
+ private boolean isValidLocale(Locale locale) {
+ Boolean isValid;
+ try {
+ isValid = locale.getLanguage().equals(Geonet.DEFAULT_LANGUAGE)
+ || ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale).getLocale().getLanguage().equals(locale.getLanguage());
+ } catch (MissingResourceException e) {
+ isValid = false;
+ }
+ if (!isValid) {
+ String localeLanguage;
+ try {
+ localeLanguage = locale.getISO3Language();
+ } catch (MissingResourceException e) {
+ localeLanguage = locale.getLanguage();
+ }
+ Log.warning(Log.GEONETWORK_MODULE + ".feedbacklanguages", "Locale '" + localeLanguage + "' is invalid or missing message bundles. Ensure feedback locales are correct.");
+ }
+ return isValid;
+ }
+
+ private String getSettingsValue(String settingName) {
+ return settingManager.getValue(settingName);
+ }
+}
diff --git a/core/src/main/java/org/fao/geonet/util/LocalizedEmail.java b/core/src/main/java/org/fao/geonet/util/LocalizedEmail.java
new file mode 100644
index 00000000000..0aa1bf978fb
--- /dev/null
+++ b/core/src/main/java/org/fao/geonet/util/LocalizedEmail.java
@@ -0,0 +1,149 @@
+//=============================================================================
+//=== 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 library is free software; you can redistribute it and/or
+//=== modify it under the terms of the GNU Lesser General Public
+//=== License as published by the Free Software Foundation; either
+//=== version 2.1 of the License, or (at your option) any later version.
+//===
+//=== This library 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
+//=== Lesser General Public License for more details.
+//===
+//=== You should have received a copy of the GNU Lesser General Public
+//=== License along with this library; 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.util;
+
+import org.apache.commons.lang.StringUtils;
+import org.fao.geonet.ApplicationContextHolder;
+import org.fao.geonet.languages.FeedbackLanguages;
+import org.fao.geonet.utils.Log;
+
+import static org.fao.geonet.util.LocalizedEmailComponent.ComponentType.*;
+import static org.fao.geonet.util.LocalizedEmailComponent.ComponentType;
+
+import java.util.*;
+
+/**
+ * Class representing a localized email.
+ */
+public class LocalizedEmail {
+ private final Boolean isHtml;
+ private final Map components;
+ private final String translationFollowsText;
+
+ private static final String SUBJECT_DELIMITER = " | ";
+ private static final String HTML_MESSAGE_DELIMITER = "
";
+ private static final String HTML_LINE_BREAK = "
";
+ private static final String TEXT_MESSAGE_DELIMITER = "\n\n--------------------------------------------------------\n\n";
+ private static final String TEXT_LINE_BREAK = "\n\n";
+
+ public LocalizedEmail(Boolean isHtml) {
+ this.isHtml = isHtml;
+
+ FeedbackLanguages feedbackLanguages = ApplicationContextHolder.get().getBean(FeedbackLanguages.class);
+ this.translationFollowsText = feedbackLanguages.getTranslationFollowsText();
+
+ this.components = new HashMap<>();
+ }
+
+ /**
+ * Add one or more components to the email object. Existing components are replaced.
+ *
+ * @param newComponents The components to add to the email.
+ */
+ public void addComponents(LocalizedEmailComponent... newComponents) {
+
+ for (LocalizedEmailComponent newComponent : newComponents) {
+
+ if (newComponent == null) {
+ throw new IllegalArgumentException("Null parameter not allowed");
+ }
+
+ components.put(newComponent.getComponentType(), newComponent);
+ }
+ }
+
+ public String getParsedSubject(Locale[] feedbackLocales) {
+ LinkedHashMap subjects = components.get(SUBJECT).getParsedMessagesMap(feedbackLocales);
+ return String.join(SUBJECT_DELIMITER, subjects.values());
+ }
+
+ public String getParsedMessage(Locale[] feedbackLocales) {
+ return getParsedMessage(feedbackLocales, null);
+ }
+
+ public String getParsedMessage(Locale[] feedbackLocales, Map replacements) {
+ LinkedHashMap messages = components.get(MESSAGE).getParsedMessagesMap(feedbackLocales, true);
+
+ // Prepend the message with a salutation placeholder if the salutation component is present
+ if (components.containsKey(SALUTATION) && components.get(SALUTATION) != null) {
+
+ LinkedHashMap salutations = components.get(SALUTATION).getParsedMessagesMap(feedbackLocales);
+ LinkedHashMap messagesWithSalutations = new LinkedHashMap<>();
+
+ for (Map.Entry entry : messages.entrySet()) {
+ //Skip messages that have no matching salutation
+ if (!salutations.containsKey(entry.getKey())) {
+ continue;
+ }
+
+ String message = entry.getValue();
+ String salutation = salutations.get(entry.getKey());
+
+ if (replacements != null && !replacements.isEmpty()) {
+ for (Map.Entry replacement : replacements.entrySet()) {
+ salutation = salutation.replace(replacement.getKey(), replacement.getValue());
+ }
+ }
+
+ messagesWithSalutations.put(entry.getKey(), salutation + message);
+ }
+
+ messages = messagesWithSalutations;
+
+ }
+
+ String messageDelimiter;
+ String lineBreak;
+
+ // Set the delimiter and break string to use based on email type
+ if (isHtml) {
+ messageDelimiter = HTML_MESSAGE_DELIMITER;
+ lineBreak = HTML_LINE_BREAK;
+ // Wrap each message in a div with a lang attribute for accessibility
+ messages.replaceAll((locale, message) -> "" + message + "
");
+ } else {
+ messageDelimiter = TEXT_MESSAGE_DELIMITER;
+ lineBreak = TEXT_LINE_BREAK;
+ }
+
+ String emailMessage = String.join(messageDelimiter, messages.values());
+
+ // Prepend the message with the translation follows text if there is more than one language specified
+ if (messages.size() > 1 && !StringUtils.isBlank(translationFollowsText)) {
+ emailMessage = translationFollowsText + lineBreak + emailMessage;
+ }
+
+ // If the email is html wrap the content in html and body tags
+ if (isHtml) {
+ if (emailMessage.contains("") || emailMessage.contains("")) {
+ Log.warning(Log.GEONETWORK_MODULE + ".localizedemail","Multilingual emails are unsupported for HTML emails with messages containing or tags. Reverting to first specified locale.");
+ return messages.get(feedbackLocales[0]);
+ }
+ emailMessage = "" + emailMessage + "";
+ }
+
+ return emailMessage;
+ }
+}
+
diff --git a/core/src/main/java/org/fao/geonet/util/LocalizedEmailComponent.java b/core/src/main/java/org/fao/geonet/util/LocalizedEmailComponent.java
new file mode 100644
index 00000000000..fa61f8e07f8
--- /dev/null
+++ b/core/src/main/java/org/fao/geonet/util/LocalizedEmailComponent.java
@@ -0,0 +1,372 @@
+//=============================================================================
+//=== 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 library is free software; you can redistribute it and/or
+//=== modify it under the terms of the GNU Lesser General Public
+//=== License as published by the Free Software Foundation; either
+//=== version 2.1 of the License, or (at your option) any later version.
+//===
+//=== This library 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
+//=== Lesser General Public License for more details.
+//===
+//=== You should have received a copy of the GNU Lesser General Public
+//=== License along with this library; 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.util;
+
+import org.fao.geonet.ApplicationContextHolder;
+import org.fao.geonet.kernel.search.JSONLocCacheLoader;
+import org.fao.geonet.kernel.setting.SettingManager;
+
+import java.text.MessageFormat;
+import java.util.*;
+
+import static org.fao.geonet.util.LocalizedEmailComponent.ReplacementType.*;
+
+/**
+ * This class is used to handle email parameters used to format localized email messages
+ */
+public class LocalizedEmailComponent {
+
+ private final ComponentType componentType;
+ private final String keyOrRawValue;
+ private final KeyType keyType;
+ private final ReplacementType replacementType;
+ private final Map> parameters;
+ private Boolean compileWithIndexFields;
+ private String metadataUuid;
+ private Boolean replaceLinks;
+ private Boolean replaceLinksWithHtmlFormat = false;
+
+ /**
+ * Enum representing the types of components in an email.
+ *
+ * This enum defines four types of components:
+ *
+ * - {@link ComponentType#SUBJECT SUBJECT}: The email subject field.
+ * - {@link ComponentType#MESSAGE MESSAGE}: The email body.
+ * - {@link ComponentType#SALUTATION SALUTATION}: The salutation to prepend each localized message with. (Ex. 'Hello John')
+ * - {@link ComponentType#NESTED NESTED}: A component of insignificant type that is used to generate other components.
+ *
+ */
+ public enum ComponentType {
+ /**
+ * The email subject field.
+ */
+ SUBJECT,
+
+ /**
+ * The email body.
+ */
+ MESSAGE,
+
+ /**
+ * The salutation to prepend each localized message with. (Ex. 'Hello John').
+ */
+ SALUTATION,
+
+ /**
+ * A component of insignificant type that is used to generate other components.
+ */
+ NESTED
+ }
+
+ /**
+ * Enum representing the types of keys used to parse a components message.
+ *
+ * This enum defines four types of keys:
+ *
+ * - {@link KeyType#MESSAGE_OR_JSON_KEY MESSAGE_OR_JSON_KEY}: Represents a component that tries to retrieve its value using {@link ResourceBundle#getString} or JSON localization files if message key was not found.
+ * - {@link KeyType#MESSAGE_KEY MESSAGE_KEY}: Represents a component that retrieves its value using {@link ResourceBundle#getString}.
+ * - {@link KeyType#JSON_KEY JSON_KEY}: Represents a component that retrieves its value by searching the JSON localization files for the specified key.
+ * - {@link KeyType#RAW_VALUE RAW_VALUE}: Represents a component in which keys are not required. The raw value from keyOrRawValue is used.
+ *
+ *
+ */
+ public enum KeyType {
+ /**
+ * Represents a component that tries to retrieve its value using {@link ResourceBundle#getString} or JSON localization files if message key was not found.
+ */
+ MESSAGE_OR_JSON_KEY,
+
+ /**
+ * Represents a component that retrieves its value using {@link ResourceBundle#getString}.
+ */
+ MESSAGE_KEY,
+
+ /**
+ * Represents a component that retrieves its value by searching the JSON localization files for the specified key.
+ */
+ JSON_KEY,
+
+ /**
+ * Represents a component in which keys are not required. The raw value from keyOrRawValue is used.
+ */
+ RAW_VALUE
+ }
+
+ /**
+ * Enum representing the types of replacements performed on the email component.
+ *
+ * This enum defines four types of replacement:
+ *
+ * - {@link ReplacementType#POSITIONAL_FORMAT POSITIONAL_FORMAT}: A parameter that retrieves its value using {@link ResourceBundle#getString}.
+ * The value property is set to the message key to search for.
+ * - {@link ReplacementType#NUMERIC_FORMAT NUMERIC_FORMAT}: A parameter that retrieves its value by searching the JSON localization files for the specified key.
+ * The value property is set to the json key to search for.
+ * - {@link ReplacementType#NAMED_FORMAT NAMED_FORMAT}: A parameter that retrieves its value using {@link XslUtil#getIndexField}.
+ * The value property is set to the field name to search for, and the uuid property is set to the record uuid to search for (required).
+ * - {@link ReplacementType#NONE NONE}: For components that require no replacement to compute their values.
+ *
+ *
+ */
+ public enum ReplacementType {
+ /**
+ * For {@link String#format}, where parameters are replaced based on their position (Ex. %s).
+ * The parameter id stores an integer representing the order of the parameters.
+ */
+ POSITIONAL_FORMAT,
+
+ /**
+ * For {@link MessageFormat#format}, where parameters are replaced based on position (Ex. {0}).
+ * The parameter id stores an integer representing the order of the parameters.
+ */
+ NUMERIC_FORMAT,
+
+ /**
+ * For {@link String#replace}, where parameters are replaced based on their names ({{title}}).
+ * The parameter id stores the string to replace.
+ */
+ NAMED_FORMAT,
+
+ /**
+ * For components that require no replacement to compute their values.
+ */
+ NONE
+ }
+
+ /**
+ * Constructor for LocalizedEmailParameters.
+ *
+ * @param replacementType the type of template variable
+ */
+ public LocalizedEmailComponent(ComponentType componentType, String keyOrRawValue, KeyType keyType, ReplacementType replacementType) {
+ this.componentType = componentType;
+ this.keyOrRawValue = keyOrRawValue;
+ this.keyType = keyType;
+ this.replacementType = replacementType;
+ this.parameters = new HashMap<>();
+ this.compileWithIndexFields = false;
+ this.metadataUuid = null;
+ this.replaceLinks = false;
+ }
+
+ /**
+ * Adds parameters to the email parameters list.
+ *
+ * @param newParameters the parameters to add
+ * @throws IllegalArgumentException if a null parameter is passed or if a duplicate parameter id is found
+ */
+ public void addParameters(Locale locale, LocalizedEmailParameter... newParameters) {
+ // If the map does not have the locale as a key add it
+ if (!parameters.containsKey(locale)) {
+ parameters.put(locale, new ArrayList<>());
+ }
+
+ for (LocalizedEmailParameter newParameter : newParameters) {
+
+ if (newParameter == null) {
+ throw new IllegalArgumentException("Null parameter not allowed");
+ }
+
+ // If the parameter id is already in the list
+ if (parameters.get(locale).stream().anyMatch(existingParameter -> newParameter.getId().equals(existingParameter.getId()))) {
+ throw new IllegalArgumentException("Duplicate parameter id: " + newParameter.getId());
+ }
+
+ // If the type of parameters are positional and the new parameters id is not an integer
+ if ((replacementType.equals(POSITIONAL_FORMAT) || replacementType.equals(NUMERIC_FORMAT)) && !(newParameter.getId() instanceof Integer)) {
+ throw new IllegalArgumentException("Positional parameter id must be an integer");
+ }
+
+ parameters.get(locale).add(newParameter);
+ }
+ }
+
+ /**
+ * @return the map of locales to lists of email parameters
+ */
+ public Map> getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Enables the compilation with index fields and sets the metadata UUID.
+ *
+ * @param metadataUuid the metadata UUID
+ */
+ public void enableCompileWithIndexFields(String metadataUuid) {
+ this.compileWithIndexFields = true;
+ this.metadataUuid = metadataUuid;
+ }
+
+ /**
+ * Sets the replace links flag and format.
+ *
+ * @param useHtmlFormat replace links using the HTML format instead of the text format.
+ */
+ public void enableReplaceLinks(Boolean useHtmlFormat) {
+ this.replaceLinks = true;
+ this.replaceLinksWithHtmlFormat = useHtmlFormat;
+ }
+
+ /**
+ * @return The type of the component.
+ */
+ public ComponentType getComponentType() {
+ return componentType;
+ }
+
+ /**
+ * Parses the message based on the provided key or template and locale.
+ *
+ * @param locale the locale
+ * @return the parsed message
+ * @throws RuntimeException if an unsupported template variable type is encountered
+ */
+ public String parseMessage(Locale locale) {
+
+ ArrayList parametersForLocale = parameters.get(locale);
+
+ String parsedMessage;
+ switch (keyType) {
+ case MESSAGE_OR_JSON_KEY:
+ try {
+ parsedMessage = getResourceBundleString(locale);
+ } catch (MissingResourceException missingResourceException) {
+ parsedMessage = getTranslationMapString(locale);
+ }
+ break;
+ case MESSAGE_KEY:
+ try {
+ parsedMessage = getResourceBundleString(locale);
+ } catch (MissingResourceException e) {
+ parsedMessage = keyOrRawValue;
+ }
+ break;
+ case JSON_KEY:
+ parsedMessage = getTranslationMapString(locale);
+ break;
+ case RAW_VALUE:
+ parsedMessage = keyOrRawValue;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported key type: " + keyType);
+ }
+
+ // Handle replacements
+ if (replacementType == POSITIONAL_FORMAT || replacementType == NUMERIC_FORMAT) {
+
+ Object[] parsedLocaleEmailParameters = parametersForLocale.stream()
+ .sorted(Comparator.comparing(parameter -> (Integer) parameter.getId()))
+ .map(parameter -> parameter.parseValue(locale))
+ .toArray();
+
+ if (replacementType == POSITIONAL_FORMAT) {
+ parsedMessage = String.format(parsedMessage, parsedLocaleEmailParameters);
+ } else {
+ // Replace the link placeholders with index field placeholder so that it isn't interpreted as a MessageFormat arg
+ if (replaceLinks) {
+ parsedMessage = replaceLinks(parsedMessage);
+ }
+ parsedMessage = MessageFormat.format(parsedMessage, parsedLocaleEmailParameters);
+ }
+
+ } else if (replacementType == NAMED_FORMAT) {
+
+ for (LocalizedEmailParameter parameter : parametersForLocale) {
+ parsedMessage = parsedMessage.replace(parameter.getId().toString(), parameter.parseValue(locale));
+ }
+
+ }
+
+ // Replace link placeholders
+ if (replaceLinks) {
+ parsedMessage = replaceLinks(parsedMessage);
+ }
+
+ // Replace index field placeholders
+ if (compileWithIndexFields && metadataUuid != null) {
+ parsedMessage = MailUtil.compileMessageWithIndexFields(parsedMessage, metadataUuid, locale.getLanguage());
+ }
+
+ return parsedMessage;
+ }
+
+ /**
+ * Returns a map of locales to parsed messages for the provided array of locales.
+ *
+ * @param feedbackLocales the array of locales
+ * @return the map of locales to parsed messages
+ */
+ public LinkedHashMap getParsedMessagesMap(Locale[] feedbackLocales) {
+ return getParsedMessagesMap(feedbackLocales, false);
+ }
+
+ /**
+ * Returns a map of locales to parsed messages for the provided array of locales.
+ * If flagged only distinct values are returned.
+ *
+ * @param feedbackLocales the array of locales
+ * @param distinct flag to only return messages with distinct values
+ * @return the map of locales to parsed messages
+ */
+ public LinkedHashMap getParsedMessagesMap(Locale[] feedbackLocales, Boolean distinct) {
+
+ LinkedHashMap parsedMessages = new LinkedHashMap<>();
+
+ for (Locale locale : feedbackLocales) {
+ String parsedMessage = parseMessage(locale);
+ if (!distinct || !parsedMessages.containsValue(parsedMessage)) {
+ parsedMessages.put(locale, parsedMessage);
+ }
+ }
+
+ return parsedMessages;
+ }
+
+ private String getResourceBundleString(Locale locale) {
+ return ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale).getString(keyOrRawValue);
+ }
+
+ private String getTranslationMapString(Locale locale) {
+ try {
+ Map translationMap = new JSONLocCacheLoader(ApplicationContextHolder.get(), locale.getISO3Language()).call();
+ return translationMap.getOrDefault(keyOrRawValue, keyOrRawValue);
+ } catch (Exception exception) {
+ return keyOrRawValue;
+ }
+ }
+
+ private String replaceLinks(String message) {
+
+ SettingManager settingManager = ApplicationContextHolder.get().getBean(SettingManager.class);
+
+ String newPlaceholder;
+ if (replaceLinksWithHtmlFormat) {
+ newPlaceholder = "{{index:uuid}}";
+ } else {
+ newPlaceholder = "'{{'index:uuid'}}'";
+ }
+ return message.replace("{{link}}", settingManager.getNodeURL() + "api/records/" + newPlaceholder);
+ }
+}
diff --git a/core/src/main/java/org/fao/geonet/util/LocalizedEmailParameter.java b/core/src/main/java/org/fao/geonet/util/LocalizedEmailParameter.java
new file mode 100644
index 00000000000..f68c36aec38
--- /dev/null
+++ b/core/src/main/java/org/fao/geonet/util/LocalizedEmailParameter.java
@@ -0,0 +1,179 @@
+//=============================================================================
+//=== 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 library is free software; you can redistribute it and/or
+//=== modify it under the terms of the GNU Lesser General Public
+//=== License as published by the Free Software Foundation; either
+//=== version 2.1 of the License, or (at your option) any later version.
+//===
+//=== This library 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
+//=== Lesser General Public License for more details.
+//===
+//=== You should have received a copy of the GNU Lesser General Public
+//=== License along with this library; 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.util;
+
+import org.fao.geonet.ApplicationContextHolder;
+import org.fao.geonet.kernel.search.JSONLocCacheLoader;
+
+import java.util.*;
+
+/**
+ * Class representing a parameter used in a localized email.
+ * It provides functionality to set and get parameter properties, and parse parameter values.
+ */
+public class LocalizedEmailParameter {
+ private final Object id;
+ private final ParameterType parameterType;
+ private final Object value; // (Based on Parameter type)
+ private final Object metadataUuid;
+
+ /**
+ * Enum representing different types of parameters used in a localized email context.
+ *
+ * This enum defines five types of parameters:
+ *
+ * - {@link ParameterType#MESSAGE_OR_JSON_KEY MESSAGE_OR_JSON_KEY}: A parameter that tries to retrieve its value using {@link ResourceBundle#getString} or JSON localization files if message key was not found.
+ * The value property is set to the (message or json) key to search for.
+ * - {@link ParameterType#MESSAGE_KEY MESSAGE_KEY}: A parameter that retrieves its value using {@link ResourceBundle#getString}.
+ * The value property is set to the message key to search for.
+ * - {@link ParameterType#JSON_KEY JSON_KEY}: A parameter that retrieves its value by searching the JSON localization files for the specified key.
+ * The value property is set to the json key to search for.
+ * - {@link ParameterType#INDEX_FIELD INDEX_FIELD}: A parameter that retrieves its value using {@link XslUtil#getIndexField}.
+ * The value property is set to the field name to search for, and the uuid property is set to the record uuid to search for (required).
+ * - {@link ParameterType#RAW_VALUE RAW_VALUE}: A parameter with a precomputed value that is simply returned.
+ * The value property contains the precomputed value.
+ *
+ *
+ * These types can be used to categorize parameters and define their intended use in the context of localized email parameterization.
+ */
+ public enum ParameterType {
+ /**
+ * A parameter that tries to retrieve its value using {@link ResourceBundle#getString} or JSON localization files if message key was not found.
+ * The value property is set to the (message or json) key to search for.
+ */
+ MESSAGE_OR_JSON_KEY,
+
+ /**
+ * A parameter that retrieves its value using {@link ResourceBundle#getString}
+ * The value property is set to the message key to search for.
+ */
+ MESSAGE_KEY,
+
+ /**
+ * A parameter that retrieves its value by searching the JSON localization files for the specified key.
+ * The value property is set to the json key to search for.
+ */
+ JSON_KEY,
+
+ /**
+ * A parameter that retrieves its value using {@link XslUtil#getIndexField}
+ * The value property is set to the field name to search for.
+ * The uuid property is set to the record uuid to search for and is required.
+ */
+ INDEX_FIELD,
+
+ /**
+ * A parameter with a precomputed value that is simply returned.
+ * The value property contains the precomputed value.
+ */
+ RAW_VALUE
+ }
+
+ /**
+ * Constructor with parameters.
+ *
+ * @param parameterType the type of the parameter
+ * @param id the id of the parameter
+ * @param value the value of the parameter
+ */
+ public LocalizedEmailParameter(ParameterType parameterType, Object id, Object value) {
+ this.parameterType = parameterType;
+ this.id = id;
+ this.value = value;
+ this.metadataUuid = null;
+ }
+
+ /**
+ * Constructor with parameters.
+ *
+ * @param parameterType the type of the parameter
+ * @param id the id of the parameter
+ * @param value the value of the parameter
+ * @param metadataUuid The metadata uuid to use for parsing index field values
+ */
+ public LocalizedEmailParameter(ParameterType parameterType, Object id, Object value, String metadataUuid) {
+ this.parameterType = parameterType;
+ this.id = id;
+ this.value = value;
+ this.metadataUuid = metadataUuid;
+ }
+
+ /**
+ * @return the id of the parameter
+ */
+ public Object getId() {
+ return id;
+ }
+
+ /**
+ * Parses the value of the parameter based on its type and the provided locale
+ *
+ * @param locale the locale to use to parse the value
+ * @return the parsed string value
+ */
+ public String parseValue(Locale locale) {
+
+ if (value == null) {
+ return "null";
+ }
+
+ switch (parameterType) {
+ case MESSAGE_OR_JSON_KEY:
+ try {
+ return getResourceBundleString(locale);
+ } catch (MissingResourceException missingResourceException) {
+ return getJsonTranslationMapString(locale);
+ }
+ case MESSAGE_KEY:
+ try {
+ return getResourceBundleString(locale);
+ } catch (MissingResourceException e) {
+ return value.toString();
+ }
+ case JSON_KEY:
+ return getJsonTranslationMapString(locale);
+ case INDEX_FIELD:
+ if (metadataUuid == null) throw new IllegalArgumentException("Metadata UUID is required for parameters of type INDEX_FIELD");
+ return XslUtil.getIndexField(null, metadataUuid, value, locale);
+ case RAW_VALUE:
+ return value.toString();
+ default:
+ throw new IllegalArgumentException("Unsupported parameter type: " + parameterType);
+ }
+ }
+
+ private String getResourceBundleString(Locale locale) {
+ return ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale).getString(value.toString());
+ }
+
+ private String getJsonTranslationMapString(Locale locale) {
+ try {
+ Map translationMap = new JSONLocCacheLoader(ApplicationContextHolder.get(), locale.getISO3Language()).call();
+ return translationMap.getOrDefault(value.toString(), value.toString());
+ } catch (Exception exception) {
+ return value.toString();
+ }
+ }
+}
+
diff --git a/core/src/main/resources/config-spring-geonetwork.xml b/core/src/main/resources/config-spring-geonetwork.xml
index afaf71f9686..052e4d6ae6d 100644
--- a/core/src/main/resources/config-spring-geonetwork.xml
+++ b/core/src/main/resources/config-spring-geonetwork.xml
@@ -238,6 +238,8 @@
+
+