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 @@ + +