Add Locale to Charset Mapping for Servlet containers

This commit adds a new configuration key:

    spring.http.encoding.mapping.<locale>=<charset>

This allows to specify which default charset should be used for any
given Locale, if none has been provided already in the response itself.
This applies to all supported embedded servlet containers.

Fixes gh-6453
pull/6482/head
Brian Clozel 8 years ago
parent 01f73d257f
commit a282710fc9

@ -20,11 +20,15 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.HttpEncodingProperties.Type;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.filter.OrderedCharacterEncodingFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
@ -32,10 +36,12 @@ import org.springframework.web.filter.CharacterEncodingFilter;
* in web applications.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 1.2.0
*/
@Configuration
@EnableConfigurationProperties(HttpEncodingProperties.class)
@ConditionalOnWebApplication
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
@ -56,4 +62,30 @@ public class HttpEncodingAutoConfiguration {
return filter;
}
@Bean
public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
return new LocaleCharsetMappingsCustomizer(this.properties);
}
private static class LocaleCharsetMappingsCustomizer implements EmbeddedServletContainerCustomizer, Ordered {
private final HttpEncodingProperties properties;
LocaleCharsetMappingsCustomizer(HttpEncodingProperties properties) {
this.properties = properties;
}
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (this.properties.getMapping() != null) {
container.setLocaleCharsetMappings(this.properties.getMapping());
}
}
@Override
public int getOrder() {
return 0;
}
}
}

@ -17,6 +17,8 @@
package org.springframework.boot.autoconfigure.web;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ -24,6 +26,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* Configuration properties for http encoding.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "spring.http.encoding")
@ -53,6 +56,11 @@ public class HttpEncodingProperties {
*/
private Boolean forceResponse;
/**
* Locale to Encoding mapping.
*/
private Map<Locale, Charset> mapping;
public Charset getCharset() {
return this.charset;
}
@ -85,6 +93,14 @@ public class HttpEncodingProperties {
this.forceResponse = forceResponse;
}
public Map<Locale, Charset> getMapping() {
return this.mapping;
}
public void setMapping(Map<Locale, Charset> mapping) {
this.mapping = mapping;
}
boolean shouldForce(Type type) {
Boolean force = (type == Type.REQUEST ? this.forceRequest : this.forceResponse);
if (force == null) {

@ -16,8 +16,11 @@
package org.springframework.boot.autoconfigure.web;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.Filter;
@ -27,13 +30,17 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor;
import org.springframework.boot.context.embedded.MockEmbeddedServletContainerFactory;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
@ -49,7 +56,7 @@ public class HttpEncodingAutoConfigurationTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private AnnotationConfigApplicationContext context;
private AnnotationConfigWebApplicationContext context;
@After
public void close() {
@ -135,6 +142,31 @@ public class HttpEncodingAutoConfigurationTests {
assertThat(beans.get(1)).isInstanceOf(HiddenHttpMethodFilter.class);
}
@Test
public void noLocaleCharsetMapping() {
load(EmptyConfiguration.class);
Map<String, EmbeddedServletContainerCustomizer> beans =
this.context.getBeansOfType(EmbeddedServletContainerCustomizer.class);
assertThat(beans.size()).isEqualTo(1);
assertThat(this.context.getBean(MockEmbeddedServletContainerFactory.class)
.getLocaleCharsetMappings().size()).isEqualTo(0);
}
@Test
public void customLocaleCharsetMappings() {
load(EmptyConfiguration.class, "spring.http.encoding.mapping.en:UTF-8",
"spring.http.encoding.mapping.fr_FR:UTF-8");
Map<String, EmbeddedServletContainerCustomizer> beans =
this.context.getBeansOfType(EmbeddedServletContainerCustomizer.class);
assertThat(beans.size()).isEqualTo(1);
assertThat(this.context.getBean(MockEmbeddedServletContainerFactory.class)
.getLocaleCharsetMappings().size()).isEqualTo(2);
assertThat(this.context.getBean(MockEmbeddedServletContainerFactory.class)
.getLocaleCharsetMappings().get(Locale.ENGLISH)).isEqualTo(Charset.forName("UTF-8"));
assertThat(this.context.getBean(MockEmbeddedServletContainerFactory.class)
.getLocaleCharsetMappings().get(Locale.FRANCE)).isEqualTo(Charset.forName("UTF-8"));
}
private void assertCharacterEncodingFilter(CharacterEncodingFilter actual,
String encoding, boolean forceRequestEncoding,
boolean forceResponseEncoding) {
@ -147,12 +179,14 @@ public class HttpEncodingAutoConfigurationTests {
this.context = doLoad(new Class<?>[] { config }, environment);
}
private AnnotationConfigApplicationContext doLoad(Class<?>[] configs,
private AnnotationConfigWebApplicationContext doLoad(Class<?>[] configs,
String... environment) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
EnvironmentTestUtils.addEnvironment(applicationContext, environment);
applicationContext.register(configs);
applicationContext.register(HttpEncodingAutoConfiguration.class);
applicationContext.register(MinimalWebAutoConfiguration.class,
HttpEncodingAutoConfiguration.class);
applicationContext.setServletContext(new MockServletContext());
applicationContext.refresh();
return applicationContext;
}
@ -190,4 +224,19 @@ public class HttpEncodingAutoConfigurationTests {
}
@Configuration
static class MinimalWebAutoConfiguration {
@Bean
public MockEmbeddedServletContainerFactory mockEmbeddedServletContainerFactory() {
return new MockEmbeddedServletContainerFactory();
}
@Bean
public EmbeddedServletContainerCustomizerBeanPostProcessor
embeddedServletContainerCustomizerBeanPostProcessor() {
return new EmbeddedServletContainerCustomizerBeanPostProcessor();
}
}
}

@ -18,10 +18,14 @@ package org.springframework.boot.context.embedded;
import java.io.File;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -39,6 +43,7 @@ import org.springframework.util.ClassUtils;
* @author Stephane Nicoll
* @author Ivan Sopov
* @author Eddú Meléndez
* @author Brian Clozel
* @see AbstractEmbeddedServletContainerFactory
*/
public abstract class AbstractConfigurableEmbeddedServletContainer
@ -81,6 +86,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer
private String serverHeader;
private Map<Locale, Charset> localeCharsetMappings = new HashMap<Locale, Charset>();
/**
* Create a new {@link AbstractConfigurableEmbeddedServletContainer} instance.
*/
@ -327,6 +334,19 @@ public abstract class AbstractConfigurableEmbeddedServletContainer
this.serverHeader = serverHeader;
}
/**
* Return the Locale to Charset mappings.
* @return the charset mappings
*/
public Map<Locale, Charset> getLocaleCharsetMappings() {
return this.localeCharsetMappings;
}
public void setLocaleCharsetMappings(Map<Locale, Charset> localeCharsetMappings) {
Assert.notNull(localeCharsetMappings, "localeCharsetMappings must not be null");
this.localeCharsetMappings = localeCharsetMappings;
}
/**
* Utility method that can be used by subclasses wishing to combine the specified
* {@link ServletContextInitializer} parameters with those defined in this instance.

@ -18,7 +18,10 @@ package org.springframework.boot.context.embedded;
import java.io.File;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -34,6 +37,7 @@ import org.springframework.boot.web.servlet.ServletContextInitializer;
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Brian Clozel
* @see EmbeddedServletContainerFactory
* @see EmbeddedServletContainerCustomizer
*/
@ -174,4 +178,10 @@ public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry
*/
void setServerHeader(String serverHeader);
/**
* Sets the Locale to Charset mappings.
* @param localeCharsetMappings the Locale to Charset mappings
*/
void setLocaleCharsetMappings(Map<Locale, Charset> localeCharsetMappings);
}

@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.servlet.ServletException;
@ -354,6 +355,10 @@ public class JettyEmbeddedServletContainerFactory
addJspServlet(context);
context.addBean(new JasperInitializer(context), true);
}
for (Locale locale : getLocaleCharsetMappings().keySet()) {
context.addLocaleEncoding(locale.toString(),
getLocaleCharsetMappings().get(locale).toString());
}
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
Configuration[] configurations = getWebAppContextConfigurations(context,
initializersToUse);

@ -26,6 +26,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -193,6 +194,15 @@ public class TomcatEmbeddedServletContainerFactory
context.setParentClassLoader(
this.resourceLoader != null ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
// override defaults, see org.apache.catalina.util.CharsetMapperDefault.properties
context.addLocaleEncodingMappingParameter(Locale.ENGLISH.toString(),
DEFAULT_CHARSET.displayName());
context.addLocaleEncodingMappingParameter(Locale.FRENCH.toString(),
DEFAULT_CHARSET.displayName());
for (Locale locale : getLocaleCharsetMappings().keySet()) {
context.addLocaleEncodingMappingParameter(locale.toString(),
getLocaleCharsetMappings().get(locale).toString());
}
try {
context.setUseRelativeRedirects(false);
}

@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.net.ssl.KeyManager;
@ -382,6 +383,10 @@ public class UndertowEmbeddedServletContainerFactory
File dir = getValidSessionStoreDir();
deployment.setSessionPersistenceManager(new FileSessionPersistence(dir));
}
for (Locale locale : getLocaleCharsetMappings().keySet()) {
deployment.addLocaleCharsetMapping(locale.toString(),
getLocaleCharsetMappings().get(locale).toString());
}
DeploymentManager manager = Servlets.newContainer().addDeployment(deployment);
manager.deploy();
SessionManager sessionManager = manager.getDeployment().getSessionManager();

@ -38,6 +38,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ -870,6 +872,17 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
});
}
@Test
public void localeCharsetMappingsAreConfigured() throws Exception {
AbstractEmbeddedServletContainerFactory factory = getFactory();
Map<Locale, Charset> mappings = new HashMap<Locale, Charset>();
mappings.put(Locale.GERMAN, Charset.forName("UTF-8"));
factory.setLocaleCharsetMappings(mappings);
this.container = factory.getEmbeddedServletContainer();
assertThat(getCharset(Locale.GERMAN).toString()).isEqualTo("UTF-8");
assertThat(getCharset(Locale.ITALIAN)).isNull();
}
protected abstract void addConnector(int port,
AbstractEmbeddedServletContainerFactory factory);
@ -917,6 +930,8 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
return MimeMappings.DEFAULT.getAll();
}
protected abstract Charset getCharset(Locale locale);
private void addTestTxtFile(AbstractEmbeddedServletContainerFactory factory)
throws IOException {
FileCopyUtils.copy("test",

@ -17,8 +17,10 @@
package org.springframework.boot.context.embedded.jetty;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -326,4 +328,11 @@ public class JettyEmbeddedServletContainerFactoryTests
return context.getMimeTypes().getMimeMap();
}
@Override
protected Charset getCharset(Locale locale) {
WebAppContext context = (WebAppContext) ((JettyEmbeddedServletContainer) this.container)
.getServer().getHandler();
String charsetName = context.getLocaleEncoding(locale);
return (charsetName != null) ? Charset.forName(charsetName) : null;
}
}

@ -21,6 +21,7 @@ import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -37,6 +38,7 @@ import org.apache.catalina.Valve;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.util.CharsetMapper;
import org.apache.catalina.valves.RemoteIpValve;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.junit.After;
@ -430,6 +432,15 @@ public class TomcatEmbeddedServletContainerFactoryTests
new InitialContext().lookup("java:comp/env");
}
@Test
public void defaultLocaleCharsetMappingsAreOverriden() throws Exception {
TomcatEmbeddedServletContainerFactory factory = getFactory();
this.container = factory.getEmbeddedServletContainer();
// override defaults, see org.apache.catalina.util.CharsetMapperDefault.properties
assertThat(getCharset(Locale.ENGLISH).toString()).isEqualTo("UTF-8");
assertThat(getCharset(Locale.FRENCH).toString()).isEqualTo("UTF-8");
}
@Override
protected Wrapper getJspServlet() {
Container context = ((TomcatEmbeddedServletContainer) this.container).getTomcat()
@ -446,6 +457,15 @@ public class TomcatEmbeddedServletContainerFactoryTests
"mimeMappings");
}
@Override
protected Charset getCharset(Locale locale) {
Context context = (Context) ((TomcatEmbeddedServletContainer) this.container)
.getTomcat().getHost().findChildren()[0];
CharsetMapper mapper = ((TomcatEmbeddedContext) context).getCharsetMapper();
String charsetName = mapper.getCharset(locale);
return (charsetName != null) ? Charset.forName(charsetName) : null;
}
private void assertTimeout(TomcatEmbeddedServletContainerFactory factory,
int expected) {
Tomcat tomcat = getTomcat(factory);

@ -19,9 +19,11 @@ package org.springframework.boot.context.embedded.undertow;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@ -268,4 +270,11 @@ public class UndertowEmbeddedServletContainerFactoryTests
return expectedMappings;
}
@Override
protected Charset getCharset(Locale locale) {
DeploymentInfo info = ((DeploymentManager) ReflectionTestUtils
.getField(this.container, "manager")).getDeployment().getDeploymentInfo();
String charsetName = info.getLocaleCharsetMapping().get(locale.toString());
return (charsetName != null) ? Charset.forName(charsetName) : null;
}
}

Loading…
Cancel
Save