diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyTypeEnum.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyTypeEnum.java index 714a7bf8b71e043778c8e8db630480237c91a726..2acbeb11ffad546e504368e3aa45e71584cee6d8 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyTypeEnum.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyTypeEnum.java @@ -25,6 +25,7 @@ package eu.europa.ec.edelivery.smp.config.enums; */ public enum SMPPropertyTypeEnum { STRING (".{0,2000}","Property value [%s] must be less than 2000 characters!"), + DATETIME (".{0,2000}","Property value [%s] must be less than 2000 characters!"), LIST_STRING(".{0,2000}","Property [%s] is not valid LIST_STRING type!"), MAP_STRING(".{0,2000}","Property [%s] is not valid MAP_STRING type!"), INTEGER("\\d{0,12}","Property [%s] is not valid Integer!"), diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java index 82dbb170da9ca5ee490c9136e23f97bc11912910..88956c554f8704f701d8e5c9920ce86991364f36 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java @@ -49,7 +49,7 @@ import java.time.OffsetDateTime; import java.util.Date; import static eu.europa.ec.edelivery.smp.cron.CronTriggerConfig.TRIGGER_BEAN_CREDENTIAL_ALERTS; -import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; +import static eu.europa.ec.edelivery.smp.utils.DateTimeUtils.formatOffsetDateTimeWithLocal; import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; /** @@ -239,18 +239,16 @@ public class CredentialsAlertService { String credentialId, OffsetDateTime expirationDate ) { - - String serverName = HttpUtils.getServerAddress(); + DBUser user = credential.getUser(); // add alert properties alert.addProperty(CredentialsExpirationProperties.CREDENTIAL_TYPE.name(), credentialType.name()); alert.addProperty(CredentialsExpirationProperties.CREDENTIAL_ID.name(), credentialId); - alert.addProperty(CredentialsExpirationProperties.EXPIRATION_DATETIME.name(), expirationDate); - alert.addProperty(CredentialsExpirationProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialsExpirationProperties.EXPIRATION_DATETIME.name(), formatOffsetDateTimeWithLocal(expirationDate, user.getSmpLocale())); + alert.addProperty(CredentialsExpirationProperties.REPORTING_DATETIME.name(), formatOffsetDateTimeWithLocal(expirationDate, user.getSmpLocale())); alert.addProperty(CredentialsExpirationProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); - alert.addProperty(CredentialsExpirationProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts - submitAlertMail(alert, credential.getUser()); + submitAlertMail(alert, user); // when alert about to expire - check if the next cron execution is expired // and set date sent tp null to ensure alert submission in next cron execution credentialDao.updateAlertSentForUserCredentials(credential, @@ -267,15 +265,13 @@ public class CredentialsAlertService { OffsetDateTime lastFailedLoginDate ) { LOG.info("Prepare alert for credentials [{}] ", credentialId); - String serverName = HttpUtils.getServerAddress(); // add alert properties alert.addProperty(CredentialVerificationFailedProperties.CREDENTIAL_TYPE.name(), credentialType.name()); alert.addProperty(CredentialVerificationFailedProperties.CREDENTIAL_ID.name(), credentialId); alert.addProperty(CredentialVerificationFailedProperties.FAILED_LOGIN_ATTEMPT.name(), failedLoginCount.toString()); - alert.addProperty(CredentialVerificationFailedProperties.LAST_LOGIN_FAILURE_DATETIME.name(), lastFailedLoginDate); - alert.addProperty(CredentialVerificationFailedProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialVerificationFailedProperties.LAST_LOGIN_FAILURE_DATETIME.name(), formatOffsetDateTimeWithLocal(lastFailedLoginDate, user.getSmpLocale())); + alert.addProperty(CredentialVerificationFailedProperties.REPORTING_DATETIME.name(), formatOffsetDateTimeWithLocal(alert.getReportingTime(), user.getSmpLocale())); alert.addProperty(CredentialVerificationFailedProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); - alert.addProperty(CredentialVerificationFailedProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts submitAlertMail(alert, user); @@ -289,16 +285,15 @@ public class CredentialsAlertService { OffsetDateTime lastFailedLoginDate, OffsetDateTime suspendedUtil) { - String serverName = HttpUtils.getServerAddress(); + // add alert properties alert.addProperty(CredentialSuspendedProperties.CREDENTIAL_TYPE.name(), credentialType.name()); alert.addProperty(CredentialSuspendedProperties.CREDENTIAL_ID.name(), credentialId); alert.addProperty(CredentialSuspendedProperties.FAILED_LOGIN_ATTEMPT.name(), failedLoginCount.toString()); - alert.addProperty(CredentialSuspendedProperties.LAST_LOGIN_FAILURE_DATETIME.name(), lastFailedLoginDate); - alert.addProperty(CredentialSuspendedProperties.SUSPENDED_UNTIL_DATETIME.name(), suspendedUtil); - alert.addProperty(CredentialSuspendedProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialSuspendedProperties.LAST_LOGIN_FAILURE_DATETIME.name(), formatOffsetDateTimeWithLocal(lastFailedLoginDate, user.getSmpLocale())); + alert.addProperty(CredentialSuspendedProperties.SUSPENDED_UNTIL_DATETIME.name(), formatOffsetDateTimeWithLocal(suspendedUtil, user.getSmpLocale())); + alert.addProperty(CredentialSuspendedProperties.REPORTING_DATETIME.name(), formatOffsetDateTimeWithLocal(alert.getReportingTime(), user.getSmpLocale())); alert.addProperty(CredentialSuspendedProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); - alert.addProperty(CredentialSuspendedProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts submitAlertMail(alert, user); @@ -404,7 +399,7 @@ public class CredentialsAlertService { // add alert properties alert.addProperty(CredentialsResetRequestProperties.CREDENTIAL_TYPE.name(), credentialType.name()); alert.addProperty(CredentialsResetRequestProperties.CREDENTIAL_ID.name(), credentialId); - alert.addProperty(CredentialsResetRequestProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialsResetRequestProperties.REPORTING_DATETIME.name(), formatOffsetDateTimeWithLocal(alert.getReportingTime(), user.getSmpLocale())); alert.addProperty(CredentialsResetRequestProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); alert.addProperty(CredentialsResetRequestProperties.RESET_URL.name(), resetUrlPath); alert.addProperty(CredentialsResetRequestProperties.SERVER_NAME.name(), serverName); @@ -437,13 +432,11 @@ public class CredentialsAlertService { CredentialType credentialType, String credentialId) { - String serverName = HttpUtils.getServerAddress(); // add alert properties alert.addProperty(CredentialsChangedProperties.CREDENTIAL_TYPE.name(), credentialType.name()); alert.addProperty(CredentialsChangedProperties.CREDENTIAL_ID.name(), credentialId); - alert.addProperty(CredentialsChangedProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialsChangedProperties.REPORTING_DATETIME.name(), formatOffsetDateTimeWithLocal(alert.getReportingTime(), user.getSmpLocale())); alert.addProperty(CredentialsChangedProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); - alert.addProperty(CredentialsChangedProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts submitAlertMail(alert, user); @@ -463,6 +456,9 @@ public class CredentialsAlertService { AlertLevelEnum level, AlertTypeEnum alertType) { + + String serverName = HttpUtils.getServerAddress(); + DBAlert alert = new DBAlert(); alert.setMailSubject(mailSubject); alert.setMailTo(mailTo); @@ -471,6 +467,7 @@ public class CredentialsAlertService { alert.setAlertType(alertType); alert.setAlertLevel(level); alert.setAlertStatus(AlertStatusEnum.PROCESS); + alert.addProperty(CredentialSuspendedProperties.SERVER_NAME.name(), serverName); return alert; } @@ -487,13 +484,14 @@ public class CredentialsAlertService { return; } - String mailFrom = configurationService.getAlertEmailFrom(); MailDataModel props = new MailDataModel(user.getSmpLocale(), alert); + + // add additional common properties to the model props.getModel().put(MailDataModel.CommonProperties.SMP_INSTANCE_NAME.name(), configurationService.getSMPInstanceName()); props.getModel().put(MailDataModel.CommonProperties.CURRENT_DATETIME.name(), - OffsetDateTime.now().format(ISO_DATE_TIME)); + formatOffsetDateTimeWithLocal(OffsetDateTime.now(), user.getSmpLocale())); try { mailService.sendMail(props, mailFrom, alert.getMailTo()); diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java index d0ee71f9403e259cf75cf0840b90e2862501e605..02d6ae8ab000e8a6b7702aceafc8fc064678175c 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java @@ -20,11 +20,17 @@ package eu.europa.ec.edelivery.smp.services.mail; import eu.europa.ec.edelivery.smp.data.model.DBAlert; import eu.europa.ec.edelivery.smp.data.ui.enums.AlertTypeEnum; +import eu.europa.ec.edelivery.smp.utils.DateTimeUtils; import org.apache.commons.lang3.StringUtils; +import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; +/** + * Mail data model for mail content. The class is used to create mail messages from templates and message + * translations. It contains common properties like current date time, SMP instance name, etc. + */ public class MailDataModel { private static final String DEFAULT_LANGUAGE = "en"; public enum CommonProperties { @@ -33,21 +39,30 @@ public class MailDataModel { } private final String language; private final AlertTypeEnum alertType; - Map<String, Object> model = new HashMap<>(); - + Map<String, String> model = new HashMap<>(); public MailDataModel(String language, AlertTypeEnum alertType, Map<String, Object> model) { this.language = language; this.alertType = alertType; - this.model.putAll(model); + model.forEach((key, value) -> this.model.put(key, valueToString(value))); } public MailDataModel(String language, final DBAlert alert) { this.language = language; this.alertType = alert.getAlertType(); - alert.getProperties().forEach((key, prop) -> this.model.put(key, prop.getValue())); + alert.getProperties().forEach((key, prop) -> this.model.put(key,valueToString(prop.getValue()))); } + private String valueToString(Object value) { + if (value instanceof OffsetDateTime) { + OffsetDateTime odt = (OffsetDateTime) value; + return DateTimeUtils.formatOffsetDateTimeWithLocal(odt, getLanguage(), null); + } + if (value == null) { + return ""; + } + return String.valueOf(value); + } /** * Get language of the mail. If not set, default language "en" is used. * @return language of the mail or "en" if not set @@ -60,7 +75,7 @@ public class MailDataModel { return alertType; } - public Map<String, Object> getModel() { + public Map<String, String> getModel() { return model; } } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java index 0abf9a4ac5061cfbf50782e6d31783c8db442abe..9858cc54d002ddeccb861116c9ad10bc8875993f 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java @@ -58,7 +58,7 @@ public class MailTemplateService { public String getMailHtmlContent(MailDataModel model) { InputStream templateIS = MailTemplateService.class.getResourceAsStream(MAIL_TEMPLATE); try { - Map<String, Object> modelData = new HashMap<>(); + Map<String, String> modelData = new HashMap<>(); modelData.put(MAIL_HEADER, getMailHeader(model)); modelData.put(MAIL_FOOTER, getMailFooter(model)); modelData.put(MAIL_TITLE, getMailTitle(model)); diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/AbstractResourceHandler.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/AbstractResourceHandler.java index 989b8eeda6fb96cd40620c473f8e08b0a593bf01..e5cf4f68b7995a6326cf922eeb5911aab81134af 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/AbstractResourceHandler.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/AbstractResourceHandler.java @@ -116,7 +116,7 @@ public class AbstractResourceHandler { throw new SMPRuntimeException(ErrorCode.RESOURCE_DOCUMENT_MISSING, resource.getIdentifierValue(), resource.getIdentifierScheme()); } // read and replace properties - Map<String, Object> docProp = resourceStorage.getResourceProperties(resource); + Map<String, String> docProp = resourceStorage.getResourceProperties(resource); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { StringNamedSubstitutor.resolve(new ByteArrayInputStream(content), docProp, baos, EXPECTED_RESOURCE_CHARSET); @@ -144,7 +144,7 @@ public class AbstractResourceHandler { resource.getIdentifierValue(), resource.getIdentifierScheme()); } - Map<String, Object> docProp = resourceStorage.getSubresourceProperties(resource, subresource); + Map<String, String> docProp = resourceStorage.getSubresourceProperties(resource, subresource); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { StringNamedSubstitutor.resolve(new ByteArrayInputStream(content), docProp, baos, EXPECTED_RESOURCE_CHARSET); diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/ResourceStorage.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/ResourceStorage.java index c3f65bdb9d0662eab88b90669c5e8732dc35ec82..6d762d33a3661e5cea765f87ecf3e5bf10a29027 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/ResourceStorage.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/resource/ResourceStorage.java @@ -102,14 +102,14 @@ public class ResourceStorage { return getDocumentContent(document, true); } @Transactional - public Map<String, Object> getResourceProperties(DBResource resource) { + public Map<String, String> getResourceProperties(DBResource resource) { DBDocument document = documentDao.getDocumentForResource(resource).orElseGet(null); if (document == null) { LOG.debug("Document not found for resource [{}]", resource); return Collections.emptyMap(); } - Map<String, Object> documentProperties = getDocumentProperties(document, true); + Map<String, String> documentProperties = getDocumentProperties(document, true); // then overwrite with document properties documentProperties.put(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_VALUE.getPropertyName(), resource.getIdentifierValue()); if (resource.getIdentifierScheme() != null) { @@ -119,7 +119,7 @@ public class ResourceStorage { } @Transactional - public Map<String, Object> getSubresourceProperties(DBResource resource, DBSubresource subresource) { + public Map<String, String> getSubresourceProperties(DBResource resource, DBSubresource subresource) { DBDocument document = documentDao.getDocumentForSubresource(subresource).orElseGet(null); if (document == null) { @@ -127,7 +127,7 @@ public class ResourceStorage { return Collections.emptyMap(); } - Map<String, Object> documentProperties = getDocumentProperties(document, true); + Map<String, String> documentProperties = getDocumentProperties(document, true); // add resource and subresource properties documentProperties.put(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_VALUE.getPropertyName(), resource.getIdentifierValue()); if (resource.getIdentifierScheme() != null) { @@ -147,12 +147,12 @@ public class ResourceStorage { * @param document document * @return document properties the key, value map */ - private Map<String, Object> getDocumentProperties(DBDocument document, boolean followReference) { + private Map<String, String> getDocumentProperties(DBDocument document, boolean followReference) { if (document == null) { return Collections.emptyMap(); } - Map<String, Object> documentProperties = new HashMap<>(); + Map<String, String> documentProperties = new HashMap<>(); DBDocument referenceDocument = document.getReferenceDocument(); if (followReference && referenceDocument != null) { if (Boolean.TRUE.equals(referenceDocument.getSharingEnabled())) { @@ -164,7 +164,7 @@ public class ResourceStorage { //add/overwrite with document properties documentProperties.put(DOCUMENT_NAME.getPropertyName(), document.getName()); documentProperties.put(DOCUMENT_MIMETYPE.getPropertyName(), document.getMimeType()); - documentProperties.put(DOCUMENT_VERSION.getPropertyName(), document.getCurrentVersion()); + documentProperties.put(DOCUMENT_VERSION.getPropertyName(), String.valueOf(document.getCurrentVersion())); document.getDocumentProperties().forEach(property -> documentProperties.put(property.getProperty(), property.getValue())); return documentProperties; diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/DateTimeUtils.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/DateTimeUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d1f61aca719abb9db484388818474a39a56dc67b --- /dev/null +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/DateTimeUtils.java @@ -0,0 +1,73 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2024 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; + +/** + * Utility class for date operations. + * + * @author Joze RIHTARSIC + * @since 5.1 + */ +public class DateTimeUtils { + + + private DateTimeUtils() { + // Utility class + } + + /** + * Format OffsetDateTime with locale for given ZoneId. IF ZoneId is null, system default time zone is used. + * + * @param offsetDateTime OffsetDateTime to format + * @param code Locale code (e.g. "en", "de", "it", "fr", "sl"). If null/empty, "en" is used + * @param zoneId ZoneId. If null, system default time zone is used + * @return formatted OffsetDateTime string representation + */ + public static String formatOffsetDateTimeWithLocal(OffsetDateTime offsetDateTime, String code, ZoneId zoneId) { + + if (offsetDateTime == null) { + return null; + } + // + Locale locale = new Locale(StringUtils.isBlank(code) ? "en" : code); + // Convert to ZonedDateTime using system default time zone + ZonedDateTime zonedDateTime = offsetDateTime.atZoneSameInstant(zoneId == null ? ZoneId.systemDefault() : zoneId); + // DateTimeFormatter with locale + DateTimeFormatter formatter = DateTimeFormatter. + ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.LONG) + .withLocale(locale); + + // Format OffsetDateTime + return zonedDateTime.format(formatter); + } + + public static String formatOffsetDateTimeWithLocal(OffsetDateTime offsetDateTime, String code) { + return formatOffsetDateTimeWithLocal(offsetDateTime, code, ZoneId.systemDefault()); + } + +} diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/HttpUtils.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/HttpUtils.java index 07c2ba04ec3e7b1c0e122d816fe9127668cbef11..522f6d93d51e31a64944cbc31517f94965bd9ebe 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/HttpUtils.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/HttpUtils.java @@ -23,6 +23,8 @@ import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import org.apache.commons.lang3.StringUtils; import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.AccessControlException; import java.util.Arrays; public class HttpUtils { @@ -62,7 +64,7 @@ public class HttpUtils { String serverAddress; try { serverAddress = InetAddress.getLocalHost().getHostName(); - } catch (Exception e) { + } catch (AccessControlException | UnknownHostException e) { LOG.error(e.getMessage(), e); serverAddress = StringUtils.EMPTY; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java index 1c87fb419787a10c83717fc54213665d28e0487c..c28ca5d2dc8c517b2a6dfbbb4944ad747a116114 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java @@ -53,7 +53,7 @@ public class StringNamedSubstitutor { * @param config the config to use * @return the resolved string */ - public static String resolve(String string, Map<String, Object> config) { + public static String resolve(String string, Map<String, String> config) { String charset = Charset.defaultCharset().name(); LOG.debug("Using default charset: [{}]", charset); return resolve(string, config, charset); @@ -68,7 +68,7 @@ public class StringNamedSubstitutor { * @param charset the character of the input stream * @return the resolved string */ - public static String resolve(String string, Map<String, Object> config, String charset) { + public static String resolve(String string, Map<String, String> config, String charset) { try { return resolve(new ByteArrayInputStream(string.getBytes()), config, charset); } catch (IOException e) { @@ -86,7 +86,7 @@ public class StringNamedSubstitutor { * @return the resolved string * @throws IOException if an I/O error occurs */ - public static String resolve(InputStream templateIS, Map<String, Object> config) throws IOException { + public static String resolve(InputStream templateIS, Map<String, String> config) throws IOException { String charset = Charset.defaultCharset().name(); LOG.debug("Using default charset: [{}]", charset); return resolve(templateIS, config, charset); @@ -101,7 +101,7 @@ public class StringNamedSubstitutor { * @param charset the character of the input stream * @return the resolved string */ - public static String resolve(InputStream templateIS, Map<String, Object> config, String charset) throws IOException { + public static String resolve(InputStream templateIS, Map<String, String> config, String charset) throws IOException { try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { resolve(templateIS, config, byteArrayOutputStream, charset); @@ -119,9 +119,9 @@ public class StringNamedSubstitutor { * @param charset the charset to use * @throws IOException if an I/O error occurs */ - public static void resolve(InputStream templateIS, Map<String, Object> config, + public static void resolve(InputStream templateIS, Map<String, String> config, OutputStream outputStream, String charset) throws IOException { - Map<String, Object> lowerCaseMap = normalizeData(config); + Map<String, String> lowerCaseMap = normalizeData(config); try (BufferedReader template = new BufferedReader(new InputStreamReader(templateIS, charset)); Writer writer = new OutputStreamWriter(outputStream, charset)) { int read; @@ -137,11 +137,9 @@ public class StringNamedSubstitutor { writer.write(START_NAME); } else { String key = lowerCase(name); - Object objValue = lowerCaseMap.get(key); - String value = objValue != null ? String.valueOf(lowerCaseMap.get(key)) : null; - - if (value != null) { - writer.write(value); + String objValue = lowerCaseMap.get(key); + if (objValue != null) { + writer.write(objValue); } else { writer.write(START_NAME); writer.write(name); @@ -158,8 +156,8 @@ public class StringNamedSubstitutor { * @param dataModel the data model * @return the normalized data model */ - private static Map<String, Object> normalizeData(Map<String, Object> dataModel) { - Map<String, Object> lowerCaseMap = new HashMap<>(); + private static Map<String, String> normalizeData(Map<String, String> dataModel) { + Map<String, String> lowerCaseMap = new HashMap<>(); // Note: do not use stream with Collectors.toMap because it throws NPE if value is null dataModel.forEach((key, value) -> lowerCaseMap.put(lowerCase(trim(key)), value)); diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIDocumentServiceTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIDocumentServiceTest.java index c078cbb5d9327a9270527c8f42092ef04ca3381b..d3b7083a9bf9d09814614a1e6dbf6e7ba568d149 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIDocumentServiceTest.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIDocumentServiceTest.java @@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @ContextConfiguration(classes = {UIDocumentService.class, ConversionTestConfig.class, ResourceHandlerService.class, OasisSMPResource10.class, OasisSMPResource10Handler.class, OasisSMPSubresource10.class, OasisSMPSubresource10Handler.class, Subresource10Validator.class,}) -public class UIDocumentServiceTest extends AbstractServiceIntegrationTest { +class UIDocumentServiceTest extends AbstractServiceIntegrationTest { @Autowired protected UIDocumentService testInstance; @@ -181,7 +181,7 @@ public class UIDocumentServiceTest extends AbstractServiceIntegrationTest { //when DocumentRO result = testInstance.saveSubresourceDocumentForResource(subresource.getId(), subresource.getResource().getId(), testDoc); - Map<String, Object> mapProperties = result.getProperties().stream().collect(Collectors.toMap(DocumentPropertyRO::getProperty, DocumentPropertyRO::getValue)); + Map<String, String> mapProperties = result.getProperties().stream().collect(Collectors.toMap(DocumentPropertyRO::getProperty, DocumentPropertyRO::getValue)); String resolved = StringNamedSubstitutor.resolve(result.getPayload(), mapProperties); // then Assertions.assertThat(resolved).doesNotContain(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_SCHEME.getPropertyPlaceholder()); diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/DateUtilsTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/DateUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..211acecbe0f9371a4de7e65203d1e5f54db49e47 --- /dev/null +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/DateUtilsTest.java @@ -0,0 +1,28 @@ +package eu.europa.ec.edelivery.smp.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.time.OffsetDateTime; +import java.time.ZoneId; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DateUtilsTest { + + @ParameterizedTest + @CsvSource({ + "2021-06-01T12:00:00Z, US,'2021-06-01 12:00:00 UTC'", + "2021-06-01T12:00:00Z, DE, '01.06.21, 12:00:00 UTC'", + "2021-06-01T12:00:00Z, IT, '01/06/21, 12:00:00 UTC'", + "2007-12-03T10:15:30+01:00, FR, '03/12/2007 09:15:30 UTC'", + "2021-06-01T12:00:00+02, SL, '1. 06. 21 10:00:00 UTC'", + }) + void testFormatOffsetDateTimeAsLocalFormat(String offsetDateTimeStr, String locale, String expected) { + // given + OffsetDateTime offsetDateTime = OffsetDateTime.parse(offsetDateTimeStr); + ZoneId zoneId = ZoneId.of("UTC"); + // Act and Assert + assertEquals(expected, DateTimeUtils.formatOffsetDateTimeWithLocal(offsetDateTime, locale, zoneId)); + } +} diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java index 956fb612eb8447c6fbddf4e8e84b3b3937295456..2267db16b73a3bda01fc5dc67d67800be232ed5e 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java @@ -59,7 +59,7 @@ class StringNamedSubstitutorTest { "'The quick red fox jumps over the ${DOG_MODE} dog'", }) void resolve(String testString, String values, String expected) { - Map<String, Object> mapVal = Stream.of(values.split("\\s*;\\s*")) + Map<String, String> mapVal = Stream.of(values.split("\\s*;\\s*")) .map(s -> s.split("\\s*=\\s*")) .collect(HashMap::new, (m, v) -> m.put(v[0], v[1]), Map::putAll); @@ -69,7 +69,7 @@ class StringNamedSubstitutorTest { @Test void testTransientResolutionForResourceWithUTF8() throws UnsupportedEncodingException { - Map<String, Object> mapProperties = new HashMap<>(); + Map<String, String> mapProperties = new HashMap<>(); mapProperties.put(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_VALUE.getPropertyName(), "value"); mapProperties.put(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_SCHEME.getPropertyName(), "scheme"); String serviceGroupWithUt8 = URLDecoder.decode(SERVICE_GROUP_WITH_UTF8, "UTF-8"); @@ -87,7 +87,7 @@ class StringNamedSubstitutorTest { @Test void testTransientResolutionWithUTF8String() throws UnsupportedEncodingException { - Map<String, Object> mapProperties = new HashMap<>(); + Map<String, String> mapProperties = new HashMap<>(); mapProperties.put(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_VALUE.getPropertyName(), "value"); mapProperties.put(TransientDocumentPropertyType.RESOURCE_IDENTIFIER_SCHEME.getPropertyName(), "scheme"); String serviceGroupWithUt8 = URLDecoder.decode(TEST_UTF8_STRING, "UTF-8"); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/cas/SMPCasUserService.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/cas/SMPCasUserService.java index 5b22b2f9f0cd53ba74410237b17c9e224c397b38..f8e04881a4153e3a7aa11facf5916a6641c04b35 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/cas/SMPCasUserService.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/cas/SMPCasUserService.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -39,6 +39,7 @@ import org.springframework.stereotype.Component; import java.util.Collections; import java.util.Map; +import java.util.stream.Collectors; /** @@ -50,82 +51,91 @@ import java.util.Map; */ @Component public class SMPCasUserService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> { - enum MappingData { - EMAIL("${email}"), - FULL_NAME("${firstName} ${lastName}"); - - private final String defaultValue; - - MappingData(String defaultValue) { - this.defaultValue = defaultValue; - } - - public String getDefaultValue() { - return defaultValue; - } - } - - private static final Logger LOG = LoggerFactory.getLogger(SMPCasUserService.class); - - private final UIUserService uiUserService; - private final ConfigurationService configurationService; - - @Autowired - public SMPCasUserService(UIUserService uiUserService, - ConfigurationService configurationService) { - this.uiUserService = uiUserService; - this.configurationService = configurationService; - } - - /** - * @param token The pre-authenticated authentication token from the cas SMPCas20ServiceTicketValidator - * @return UserDetails for the given authentication token, never null. - * @throws UsernameNotFoundException if no user details can be found for the given authentication token - */ - @Override - public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException { - - AttributePrincipal principal = token.getAssertion().getPrincipal(); - // the cas id must match with username - - String username = principal.getName(); - LOG.info("Got CAS user with principal name: [{}]", username); + enum MappingData { + EMAIL("${email}"), + FULL_NAME("${firstName} ${lastName}"); + + private final String defaultValue; + + MappingData(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getDefaultValue() { + return defaultValue; + } + } + + private static final Logger LOG = LoggerFactory.getLogger(SMPCasUserService.class); + + private final UIUserService uiUserService; + private final ConfigurationService configurationService; + + @Autowired + public SMPCasUserService(UIUserService uiUserService, + ConfigurationService configurationService) { + this.uiUserService = uiUserService; + this.configurationService = configurationService; + } + + /** + * @param token The pre-authenticated authentication token from the cas SMPCas20ServiceTicketValidator + * @return UserDetails for the given authentication token, never null. + * @throws UsernameNotFoundException if no user details can be found for the given authentication token + */ + @Override + public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException { + + AttributePrincipal principal = token.getAssertion().getPrincipal(); + // the cas id must match with username + + String username = principal.getName(); + LOG.info("Got CAS user with principal name: [{}]", username); DBUser dbuser; - try { - dbuser = uiUserService.findUserByUsername(username); - } catch (SMPRuntimeException ex) { - if (configurationService.isCasAutomaticRegistrationEnabledForUserAuthentication()) { - dbuser = registerNewCasUser(principal); - }else { - throw new UsernameNotFoundException("User with the username ["+username+"] is not registered in SMP", ex); - } - } - - SMPAuthority authority = SMPAuthority.getAuthorityByApplicationRole(dbuser.getApplicationRole()); - // generate secret for the session - SMPUserDetails smpUserDetails = new SMPUserDetails(dbuser, SecurityUtils.generatePrivateSymmetricKey(true), Collections.singletonList(authority)); - smpUserDetails.setCasAuthenticated(true); - LOG.info("Return authenticated user details for username: [{}]", username); - return smpUserDetails; - } - - public DBUser registerNewCasUser(AttributePrincipal principal){ - Map<String, Object> attributes = principal.getAttributes(); - Map<String, String> attributesMap = configurationService.getCasAutomaticRegistrationDataMapping(); - - DBUser dbUser = new DBUser(); - // update user data by admin - dbUser.setUsername(principal.getName()); - dbUser.setApplicationRole(ApplicationRoleType.USER); - dbUser.setActive(!configurationService.isCasAutomaticRegistrationConfirmation()); - dbUser.setEmailAddress(getValueFromCasPrincipal(MappingData.EMAIL, attributes, attributesMap)); - dbUser.setFullName(getValueFromCasPrincipal(MappingData.FULL_NAME, attributes, attributesMap)); - uiUserService.createDBUser(dbUser); - return dbUser; - } - - protected String getValueFromCasPrincipal(MappingData data, Map<String, Object> attributes, Map<String,String> mapping){ - String template = mapping.getOrDefault(data.name(), data.getDefaultValue()); - return StringNamedSubstitutor.resolve(template, attributes); - } + try { + dbuser = uiUserService.findUserByUsername(username); + } catch (SMPRuntimeException ex) { + if (configurationService.isCasAutomaticRegistrationEnabledForUserAuthentication()) { + dbuser = registerNewCasUser(principal); + } else { + throw new UsernameNotFoundException("User with the username [" + username + "] is not registered in SMP", ex); + } + } + + SMPAuthority authority = SMPAuthority.getAuthorityByApplicationRole(dbuser.getApplicationRole()); + // generate secret for the session + SMPUserDetails smpUserDetails = new SMPUserDetails(dbuser, SecurityUtils.generatePrivateSymmetricKey(true), Collections.singletonList(authority)); + smpUserDetails.setCasAuthenticated(true); + LOG.info("Return authenticated user details for username: [{}]", username); + return smpUserDetails; + } + + public DBUser registerNewCasUser(AttributePrincipal principal) { + // convert user attributes to map of string + Map<String, Object> princAttr = principal.getAttributes(); + Map<String, String> attributes = princAttr == null ? Collections.emptyMap() : + princAttr.entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> String.valueOf(entry.getValue()) + )); + + Map<String, String> attributesMap = configurationService.getCasAutomaticRegistrationDataMapping(); + + DBUser dbUser = new DBUser(); + // update user data by admin + dbUser.setUsername(principal.getName()); + dbUser.setApplicationRole(ApplicationRoleType.USER); + dbUser.setActive(!configurationService.isCasAutomaticRegistrationConfirmation()); + dbUser.setEmailAddress(getValueFromCasPrincipal(MappingData.EMAIL, attributes, attributesMap)); + dbUser.setFullName(getValueFromCasPrincipal(MappingData.FULL_NAME, attributes, attributesMap)); + uiUserService.createDBUser(dbUser); + return dbUser; + } + + protected String getValueFromCasPrincipal(MappingData data, Map<String, String> attributes, Map<String, String> mapping) { + String template = mapping.getOrDefault(data.name(), data.getDefaultValue()); + return StringNamedSubstitutor.resolve(template, attributes); + } }