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 91ddd8d9116ed016a91646941c0542cf1691d9ad..c58f815f5f84b7bf08b1cb6065c719c9dc8b61fe 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 @@ -43,7 +43,7 @@ import java.util.Properties; public class MailTemplateService { private static final String DEFAULT_LANGUAGE = "en"; private static final String MAIL_TEMPLATE = "/mail-messages/mail-template.htm"; - + private static final String MAIL_TEMPLATE_CHARSET= "UTF-8"; private static final String MAIL_HEADER = "MAIL_HEADER"; private static final String MAIL_FOOTER = "MAIL_FOOTER"; private static final String MAIL_TITLE = "MAIL_TITLE"; @@ -58,7 +58,7 @@ public class MailTemplateService { modelData.put(MAIL_FOOTER, getMailFooter(model)); modelData.put(MAIL_TITLE, getMailTitle(model)); modelData.put(MAIL_CONTENT, getMailBody(model)); - return StringNamedSubstitutor.resolve(templateIS, modelData); + return StringNamedSubstitutor.resolve(templateIS, modelData, MAIL_TEMPLATE_CHARSET); } catch (IOException e) { throw new SMPRuntimeException(ErrorCode.INTERNAL_ERROR, "Error reading mail template", ExceptionUtils.getRootCauseMessage(e)); } 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 fabb69722db7e1e63e1b7e0083871d8bffcc9827..456f6987542ca902631482e3291fa4d9ea82856c 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 @@ -50,6 +50,7 @@ import java.util.stream.Collectors; public class AbstractResourceHandler { protected static final SMPLogger LOG = SMPLoggerFactory.getLogger(AbstractResourceHandler.class); + private static final String EXPECTED_RESOURCE_CHARSET= "UTF-8"; // the Spring beans for the resource definitions final List<ResourceDefinitionSpi> resourceDefinitionSpiList; final ResourceStorage resourceStorage; @@ -117,7 +118,7 @@ public class AbstractResourceHandler { Map<String, Object> docProp = resourceStorage.getResourceProperties(resource); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - StringNamedSubstitutor.resolve(new ByteArrayInputStream(content), docProp, baos); + StringNamedSubstitutor.resolve(new ByteArrayInputStream(content), docProp, baos, EXPECTED_RESOURCE_CHARSET); ByteArrayInputStream inputStream = new ByteArrayInputStream(baos.toByteArray()); return buildRequestDataForResource(domain, resource, @@ -145,7 +146,7 @@ public class AbstractResourceHandler { Map<String, Object> docProp = resourceStorage.getSubresourceProperties(resource, subresource); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - StringNamedSubstitutor.resolve(new ByteArrayInputStream(content), docProp, baos); + StringNamedSubstitutor.resolve(new ByteArrayInputStream(content), docProp, baos, EXPECTED_RESOURCE_CHARSET); return new SpiRequestData(domain.getDomainCode(), SPIUtils.toUrlIdentifier(resource), SPIUtils.toUrlIdentifier(subresource), 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 f2b2c4bdf1ba86c66d95e11bb3a8235ce5b5ede8..c8a13ae24ddc9f12a5e0bd492a78d5e21a281eab 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 @@ -19,8 +19,10 @@ package eu.europa.ec.edelivery.smp.utils; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; import java.io.*; +import java.nio.charset.Charset; import java.util.Map; import java.util.stream.Collectors; @@ -33,6 +35,7 @@ import java.util.stream.Collectors; * @since 4.2 */ public class StringNamedSubstitutor { + private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(StringNamedSubstitutor.class); private static final String START_NAME = "${"; private static final char END_NAME = '}'; @@ -44,15 +47,62 @@ public class StringNamedSubstitutor { * Substitute named variables in the string with key value pairs from the map. * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . * + * @param string the string to resolve + * @param config the config to use + * @return the resolved string + */ + public static String resolve(String string, Map<String, Object> config) { + String charset = Charset.defaultCharset().name(); + LOG.debug("Using default charset: [{}]", charset); + return resolve(string, config, charset); + } + /** + * Substitute named variables in the string with key value pairs from the map. + * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . + * + * @param string the string to resolve + * @param config the config to use + * @param charset the character of the input stream + * @return the resolved string + */ + public static String resolve(String string, Map<String, Object> config, String charset) { + try { + return resolve(new ByteArrayInputStream(string.getBytes()), config, charset); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Substitute named variables in the string with key value pairs from the map. + * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . + * The input stream is ready with default charset. + * * @param templateIS the InputStream to resolve - * @param config the config to use + * @param config the map of property names and its values * @return the resolved string + * @throws IOException if an I/O error occurs */ public static String resolve(InputStream templateIS, Map<String, Object> config) throws IOException { + String charset = Charset.defaultCharset().name(); + LOG.debug("Using default charset: [{}]", charset); + return resolve(templateIS, config, charset); + } + + /** + * Substitute named variables in the string with key value pairs from the map. + * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . + * + * @param templateIS the InputStream to resolve + * @param config the config to use + * @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 { Map<String, Object> lowerCaseMap = config.entrySet().stream() .collect(Collectors.toMap(e -> StringUtils.lowerCase(e.getKey()), Map.Entry::getValue)); try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { - resolve(templateIS, lowerCaseMap, byteArrayOutputStream); + resolve(templateIS, lowerCaseMap, byteArrayOutputStream, charset); return byteArrayOutputStream.toString(); } } @@ -64,42 +114,45 @@ public class StringNamedSubstitutor { * @param templateIS the InputStream to resolve * @param config the config to use * @param outputStream the output stream to write the resolved string + * @param charset the charset to use * @throws IOException if an I/O error occurs */ - public static void resolve(InputStream templateIS, Map<String, Object> config, OutputStream outputStream) throws IOException { + public static void resolve(InputStream templateIS, Map<String, Object> config, + OutputStream outputStream, String charset) throws IOException { Map<String, Object> lowerCaseMap = config.entrySet().stream() .collect(Collectors.toMap(e -> StringUtils.lowerCase(e.getKey()), Map.Entry::getValue)); - try (BufferedReader template = new BufferedReader(new InputStreamReader(templateIS))) { + try (BufferedReader template = new BufferedReader(new InputStreamReader(templateIS, charset)); + Writer writer = new OutputStreamWriter(outputStream, charset)) { int read; while ((read = template.read()) != -1) { if (read != START_NAME.charAt(0) || !isStartSequence(template)) { - outputStream.write((char) read); + writer.write((char) read); continue; } template.skip(1L); String name = readName(template, END_NAME); if (name == null) { - outputStream.write(START_NAME.getBytes()); + writer.write(START_NAME); } else { String key = StringUtils.lowerCase(name); Object objValue = lowerCaseMap.get(key); String value = objValue != null ? String.valueOf(lowerCaseMap.get(key)) : null; if (value != null) { - outputStream.write(value.getBytes()); + writer.write(value); } else { - outputStream.write(START_NAME.getBytes()); - outputStream.write(name.getBytes()); - outputStream.write(END_NAME); + writer.write(START_NAME); + writer.write(name); + writer.write(END_NAME); } } } } } - public static boolean isStartSequence(BufferedReader reader) throws IOException { + private static boolean isStartSequence(BufferedReader reader) throws IOException { reader.mark(START_NAME.length()); int read = reader.read(); if (read == -1) { @@ -144,20 +197,4 @@ public class StringNamedSubstitutor { } return null; } - - /** - * Substitute named variables in the string with key value pairs from the map. - * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . - * - * @param string the string to resolve - * @param config the config to use - * @return the resolved string - */ - public static String resolve(String string, Map<String, Object> config) { - try { - return resolve(new ByteArrayInputStream(string.getBytes()), config); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - } } 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 8076ad8b77e08b3c5067880ebd91854acefe5d42..956fb612eb8447c6fbddf4e8e84b3b3937295456 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 @@ -18,9 +18,14 @@ */ package eu.europa.ec.edelivery.smp.utils; +import eu.europa.ec.smp.spi.enums.TransientDocumentPropertyType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -29,6 +34,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class StringNamedSubstitutorTest { + private static final String TEST_UTF8_STRING = "Test%C4%85%C3%B3%C5%BC%C4%99%C4%85%E1%BA%9E%C3%B6+Greek+%C3%80%C3%86%C3%87%C3%9F%C3%A3%C3%BF%CE%B1%CE%A9%C6%92%CE%91+char"; + // partially URL encoded service group with UTF8 characters. + // test characters are URL encoded to "survive various development local settings :)" to use this first url decode string! + private static final String SERVICE_GROUP_WITH_UTF8 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" + + "<ServiceGroup xmlns=\"http://docs.oasis-open.org/bdxr/ns/SMP/2016/05\">\n" + + " <ParticipantIdentifier scheme=\"${resource.identifier.scheme}\">${resource.identifier.value}</ParticipantIdentifier>\n" + + " <ServiceMetadataReferenceCollection/>\n" + + " <Extension>\n" + + " <ex:Test xmlns:ex=\"http://test.eu\">"+TEST_UTF8_STRING+"</ex:Test>\n" + + " </Extension>\n" + + "</ServiceGroup>"; @ParameterizedTest @CsvSource({ @@ -50,4 +66,36 @@ class StringNamedSubstitutorTest { String result = StringNamedSubstitutor.resolve(testString, mapVal); assertEquals(expected, result); } + + @Test + void testTransientResolutionForResourceWithUTF8() throws UnsupportedEncodingException { + Map<String, Object> 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"); + String testStringInMessage = URLDecoder.decode(TEST_UTF8_STRING, "UTF-8"); + // when + System.out.println(serviceGroupWithUt8); + String resolved = StringNamedSubstitutor.resolve(serviceGroupWithUt8, mapProperties); + System.out.println(resolved); + //then + Assertions.assertThat(resolved) + .doesNotContain(TransientDocumentPropertyType.SUBRESOURCE_IDENTIFIER_VALUE.getPropertyPlaceholder()) + .doesNotContain(TransientDocumentPropertyType.SUBRESOURCE_IDENTIFIER_SCHEME.getPropertyPlaceholder()) + .contains(testStringInMessage); + } + + @Test + void testTransientResolutionWithUTF8String() throws UnsupportedEncodingException { + Map<String, Object> 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"); + // when + System.out.println(serviceGroupWithUt8); + String resolved = StringNamedSubstitutor.resolve(serviceGroupWithUt8, mapProperties); + System.out.println(resolved); + //then + assertEquals(serviceGroupWithUt8, resolved); + } } diff --git a/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/controllers/ResourceControllerSingleDomainTest.java b/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/controllers/ResourceControllerSingleDomainTest.java index 3ae62aa3cdae4405e1f50d908e8b12e3fe315259..1f58f212b7bdd5f23e702bc9b9522b327eef002a 100644 --- a/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/controllers/ResourceControllerSingleDomainTest.java +++ b/smp-webapp/src/test/java/eu/europa/ec/edelivery/smp/controllers/ResourceControllerSingleDomainTest.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. @@ -18,7 +18,9 @@ */ package eu.europa.ec.edelivery.smp.controllers; +import eu.europa.ec.edelivery.smp.server.security.SignatureUtil; import eu.europa.ec.edelivery.smp.ui.AbstractControllerTest; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import java.io.IOException; +import java.io.InputStream; import static eu.europa.ec.edelivery.smp.ServiceGroupBodyUtil.getSampleServiceGroupBodyWithScheme; import static java.lang.String.format; @@ -105,6 +108,23 @@ public class ResourceControllerSingleDomainTest extends AbstractControllerTest { .andExpect(status().is(expectedStatus)); } + @Test + void addServiceGroupWithUTF8() throws Exception { + String resourceLocation = "/input/ServiceGroupWithUTF8.xml"; + LOG.info(resourceLocation); + + InputStream inputStream = SignatureUtil.class.getResourceAsStream(resourceLocation); + byte[] data = IOUtils.toByteArray(inputStream); + + mvc.perform(put(URL_PATH) + .with(ADMIN_CREDENTIALS) + .contentType(APPLICATION_XML_VALUE) + .content(data)) + .andExpect(status().isCreated()); + // get service group + mvc.perform(get(URL_PATH)) + .andExpect(status().isOk()); + } @Test void anonymousUserCannotCreateServiceGroup() throws Exception { diff --git a/smp-webapp/src/test/resources/input/ServiceGroupWithUTF8.xml b/smp-webapp/src/test/resources/input/ServiceGroupWithUTF8.xml new file mode 100644 index 0000000000000000000000000000000000000000..b9231f731ace3b3b2fedcbb41ea1c7c3565bcceb --- /dev/null +++ b/smp-webapp/src/test/resources/input/ServiceGroupWithUTF8.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<ServiceGroup xmlns="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05"> + <ParticipantIdentifier scheme="${resource.identifier.scheme}">${resource.identifier.value}</ParticipantIdentifier> + <ServiceMetadataReferenceCollection/> + <Extension> + <ex:Test xmlns:ex="http://test.eu">Testąóżęąẞö Greek ÀÆÇßãÿαΩƒΑ char</ex:Test> + </Extension> +</ServiceGroup>