Moved encodeHttpHeaderFieldParam method to HttpHeaders itself (including tests)
This commit also sets the test source encoding to UTF-8. Issue: SPR-14547
This commit is contained in:
parent
bbd5993945
commit
a8f7f75f64
|
@ -123,8 +123,6 @@ configure(allprojects) { project ->
|
|||
}
|
||||
}
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
|
||||
compileJava.options*.compilerArgs = [
|
||||
"-Xlint:serial", "-Xlint:varargs", "-Xlint:cast", "-Xlint:classfile",
|
||||
"-Xlint:dep-ann", "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally",
|
||||
|
@ -143,11 +141,13 @@ configure(allprojects) { project ->
|
|||
compileJava {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
compileTestJava {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += "-parameters"
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
package org.springframework.util;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -1196,44 +1194,4 @@ public abstract class StringUtils {
|
|||
return arrayToDelimitedString(arr, ",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the given header field param as describe in the rfc5987.
|
||||
* @param input the header field param
|
||||
* @param charset the charset of the header field param string
|
||||
* @return the encoded header field param
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5987">rfc5987</a>
|
||||
* @since 5.0
|
||||
*/
|
||||
public static String encodeHttpHeaderFieldParam(String input, Charset charset) {
|
||||
Assert.notNull(charset, "charset should not be null");
|
||||
if(StandardCharsets.US_ASCII.equals(charset)) {
|
||||
return input;
|
||||
}
|
||||
Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset),
|
||||
"charset should be UTF-8 or ISO-8859-1");
|
||||
final byte[] source = input.getBytes(charset);
|
||||
final int len = source.length;
|
||||
final StringBuilder sb = new StringBuilder(len << 1);
|
||||
sb.append(charset.name());
|
||||
sb.append("''");
|
||||
for (byte b : source) {
|
||||
if (isRFC5987AttrChar(b)) {
|
||||
sb.append((char) b);
|
||||
}
|
||||
else {
|
||||
sb.append('%');
|
||||
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
|
||||
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
|
||||
sb.append(hex1);
|
||||
sb.append(hex2);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isRFC5987AttrChar(byte c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
|| c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-'
|
||||
|| c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -628,8 +628,7 @@ public class StringUtilsTests {
|
|||
assertEquals("Multi-valued variant portion of the Locale not extracted correctly.", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-3671
|
||||
@Test
|
||||
@Test // SPR-3671
|
||||
public void testParseLocaleWithMultiValuedVariant() throws Exception {
|
||||
final String variant = "proper_northern";
|
||||
final String localeString = "en_GB_" + variant;
|
||||
|
@ -637,8 +636,7 @@ public class StringUtilsTests {
|
|||
assertEquals("Multi-valued variant portion of the Locale not extracted correctly.", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-3671
|
||||
@Test
|
||||
@Test // SPR-3671
|
||||
public void testParseLocaleWithMultiValuedVariantUsingSpacesAsSeparators() throws Exception {
|
||||
final String variant = "proper northern";
|
||||
final String localeString = "en GB " + variant;
|
||||
|
@ -646,8 +644,7 @@ public class StringUtilsTests {
|
|||
assertEquals("Multi-valued variant portion of the Locale not extracted correctly.", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-3671
|
||||
@Test
|
||||
@Test // SPR-3671
|
||||
public void testParseLocaleWithMultiValuedVariantUsingMixtureOfUnderscoresAndSpacesAsSeparators() throws Exception {
|
||||
final String variant = "proper northern";
|
||||
final String localeString = "en_GB_" + variant;
|
||||
|
@ -655,8 +652,7 @@ public class StringUtilsTests {
|
|||
assertEquals("Multi-valued variant portion of the Locale not extracted correctly.", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-3671
|
||||
@Test
|
||||
@Test // SPR-3671
|
||||
public void testParseLocaleWithMultiValuedVariantUsingSpacesAsSeparatorsWithLotsOfLeadingWhitespace() throws Exception {
|
||||
final String variant = "proper northern";
|
||||
final String localeString = "en GB " + variant; // lots of whitespace
|
||||
|
@ -664,8 +660,7 @@ public class StringUtilsTests {
|
|||
assertEquals("Multi-valued variant portion of the Locale not extracted correctly.", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-3671
|
||||
@Test
|
||||
@Test // SPR-3671
|
||||
public void testParseLocaleWithMultiValuedVariantUsingUnderscoresAsSeparatorsWithLotsOfLeadingWhitespace() throws Exception {
|
||||
final String variant = "proper_northern";
|
||||
final String localeString = "en_GB_____" + variant; // lots of underscores
|
||||
|
@ -673,8 +668,7 @@ public class StringUtilsTests {
|
|||
assertEquals("Multi-valued variant portion of the Locale not extracted correctly.", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-7779
|
||||
@Test
|
||||
@Test // SPR-7779
|
||||
public void testParseLocaleWithInvalidCharacters() {
|
||||
try {
|
||||
StringUtils.parseLocaleString("%0D%0AContent-length:30%0D%0A%0D%0A%3Cscript%3Ealert%28123%29%3C/script%3E");
|
||||
|
@ -685,15 +679,13 @@ public class StringUtilsTests {
|
|||
}
|
||||
}
|
||||
|
||||
// SPR-9420
|
||||
@Test
|
||||
@Test // SPR-9420
|
||||
public void testParseLocaleWithSameLowercaseTokenForLanguageAndCountry() {
|
||||
assertEquals("tr_TR", StringUtils.parseLocaleString("tr_tr").toString());
|
||||
assertEquals("bg_BG_vnt", StringUtils.parseLocaleString("bg_bg_vnt").toString());
|
||||
}
|
||||
|
||||
// SPR-11806
|
||||
@Test
|
||||
@Test // SPR-11806
|
||||
public void testParseLocaleWithVariantContainingCountryCode() {
|
||||
String variant = "GBtest";
|
||||
String localeString = "en_GB_" + variant;
|
||||
|
@ -701,19 +693,4 @@ public class StringUtilsTests {
|
|||
assertEquals("Variant containing country code not extracted correctly", variant, locale.getVariant());
|
||||
}
|
||||
|
||||
// SPR-14547
|
||||
@Test
|
||||
public void encodeHttpHeaderFieldParam() {
|
||||
String result = StringUtils.encodeHttpHeaderFieldParam("test.txt", StandardCharsets.US_ASCII);
|
||||
assertEquals("test.txt", result);
|
||||
|
||||
result = StringUtils.encodeHttpHeaderFieldParam("中文.txt", StandardCharsets.UTF_8);
|
||||
assertEquals("UTF-8''%E4%B8%AD%E6%96%87.txt", result);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void encodeHttpHeaderFieldParamInvalidCharset() {
|
||||
StringUtils.encodeHttpHeaderFieldParam("test", StandardCharsets.UTF_16);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -679,13 +679,14 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
|||
|
||||
/**
|
||||
* Set the (new) value of the {@code Content-Disposition} header
|
||||
* for {@code form-data}, optionally encoding the filename using the rfc5987.
|
||||
* for {@code form-data}, optionally encoding the filename using the RFC 5987.
|
||||
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
|
||||
* @param name the control name
|
||||
* @param filename the filename (may be {@code null})
|
||||
* @param charset the charset used for the filename (may be {@code null})
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.4">rfc7230 Section 3.2.4</a>
|
||||
* @since 5.0
|
||||
* @since 4.3.3
|
||||
* @see #setContentDispositionFormData(String, String)
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.4">RFC 7230 Section 3.2.4</a>
|
||||
*/
|
||||
public void setContentDispositionFormData(String name, String filename, Charset charset) {
|
||||
Assert.notNull(name, "'name' must not be null");
|
||||
|
@ -698,7 +699,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
|||
}
|
||||
else {
|
||||
builder.append("; filename*=");
|
||||
builder.append(StringUtils.encodeHttpHeaderFieldParam(filename, charset));
|
||||
builder.append(encodeHeaderFieldParam(filename, charset));
|
||||
}
|
||||
}
|
||||
set(CONTENT_DISPOSITION, builder.toString());
|
||||
|
@ -1345,4 +1346,45 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
|||
return new HttpHeaders(headers, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the given header field param as describe in RFC 5987.
|
||||
* @param input the header field param
|
||||
* @param charset the charset of the header field param string
|
||||
* @return the encoded header field param
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
|
||||
*/
|
||||
static String encodeHeaderFieldParam(String input, Charset charset) {
|
||||
Assert.notNull(input, "Input String should not be null");
|
||||
Assert.notNull(charset, "Charset should not be null");
|
||||
if (StandardCharsets.US_ASCII.equals(charset)) {
|
||||
return input;
|
||||
}
|
||||
Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset),
|
||||
"Charset should be UTF-8 or ISO-8859-1");
|
||||
byte[] source = input.getBytes(charset);
|
||||
int len = source.length;
|
||||
StringBuilder sb = new StringBuilder(len << 1);
|
||||
sb.append(charset.name());
|
||||
sb.append("''");
|
||||
for (byte b : source) {
|
||||
if (isRFC5987AttrChar(b)) {
|
||||
sb.append((char) b);
|
||||
}
|
||||
else {
|
||||
sb.append('%');
|
||||
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
|
||||
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
|
||||
sb.append(hex1);
|
||||
sb.append(hex2);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isRFC5987AttrChar(byte c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' ||
|
||||
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -150,7 +150,6 @@ public class HttpHeadersTests {
|
|||
headers.setHost(host);
|
||||
assertEquals("Invalid Host header", host, headers.getHost());
|
||||
assertEquals("Invalid Host header", "localhost:8080", headers.getFirst("Host"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -159,7 +158,6 @@ public class HttpHeadersTests {
|
|||
headers.setHost(host);
|
||||
assertEquals("Invalid Host header", host, headers.getHost());
|
||||
assertEquals("Invalid Host header", "localhost", headers.getFirst("Host"));
|
||||
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
|
@ -429,4 +427,18 @@ public class HttpHeadersTests {
|
|||
assertEquals(HttpMethod.POST, headers.getAccessControlRequestMethod());
|
||||
}
|
||||
|
||||
@Test // SPR-14547
|
||||
public void encodeHeaderFieldParam() {
|
||||
String result = HttpHeaders.encodeHeaderFieldParam("test.txt", StandardCharsets.US_ASCII);
|
||||
assertEquals("test.txt", result);
|
||||
|
||||
result = HttpHeaders.encodeHeaderFieldParam("中文.txt", StandardCharsets.UTF_8);
|
||||
assertEquals("UTF-8''%E4%B8%AD%E6%96%87.txt", result);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void encodeHeaderFieldParamInvalidCharset() {
|
||||
HttpHeaders.encodeHeaderFieldParam("test", StandardCharsets.UTF_16);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -45,7 +45,7 @@ import org.springframework.web.util.NestedServletException;
|
|||
* @see GroovyMarkupViewResolver
|
||||
* @see GroovyMarkupConfigurer
|
||||
* @see <a href="http://groovy-lang.org/templating.html#_the_markuptemplateengine">
|
||||
* Groovy Markup Template engine documentation</a>
|
||||
* Groovy Markup Template engine documentation</a>
|
||||
*/
|
||||
public class GroovyMarkupView extends AbstractTemplateView {
|
||||
|
||||
|
@ -63,17 +63,6 @@ public class GroovyMarkupView extends AbstractTemplateView {
|
|||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkResource(Locale locale) throws Exception {
|
||||
try {
|
||||
this.engine.resolveTemplate(getUrl());
|
||||
}
|
||||
catch (IOException exception) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked at startup.
|
||||
* If no {@link #setTemplateEngine(MarkupTemplateEngine) templateEngine} has
|
||||
|
@ -107,6 +96,17 @@ public class GroovyMarkupView extends AbstractTemplateView {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean checkResource(Locale locale) throws Exception {
|
||||
try {
|
||||
this.engine.resolveTemplate(getUrl());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderMergedTemplateModel(Map<String, Object> model,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
@ -125,8 +125,9 @@ public class GroovyMarkupView extends AbstractTemplateView {
|
|||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
Throwable cause = (ex.getCause() != null ? ex.getCause() : ex);
|
||||
throw new NestedServletException("Could not find class while rendering Groovy Markup view with name '" +
|
||||
getUrl() + "': " + ex.getMessage() + "'", cause);
|
||||
throw new NestedServletException(
|
||||
"Could not find class while rendering Groovy Markup view with name '" +
|
||||
getUrl() + "': " + ex.getMessage() + "'", cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,7 @@
|
|||
*/
|
||||
package org.springframework.web.servlet.view.groovy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
@ -27,7 +25,6 @@ import groovy.text.Template;
|
|||
import groovy.text.TemplateEngine;
|
||||
import groovy.text.markup.MarkupTemplateEngine;
|
||||
import groovy.text.markup.TemplateConfiguration;
|
||||
import org.codehaus.groovy.control.CompilationFailedException;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
@ -57,6 +54,7 @@ public class GroovyMarkupViewTests {
|
|||
|
||||
private ServletContext servletContext;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.webAppContext = mock(WebApplicationContext.class);
|
||||
|
@ -82,7 +80,6 @@ public class GroovyMarkupViewTests {
|
|||
|
||||
@Test
|
||||
public void customTemplateEngine() throws Exception {
|
||||
|
||||
GroovyMarkupView view = new GroovyMarkupView();
|
||||
view.setTemplateEngine(new TestTemplateEngine());
|
||||
view.setApplicationContext(this.webAppContext);
|
||||
|
@ -95,9 +92,7 @@ public class GroovyMarkupViewTests {
|
|||
|
||||
@Test
|
||||
public void detectTemplateEngine() throws Exception {
|
||||
|
||||
GroovyMarkupView view = new GroovyMarkupView();
|
||||
|
||||
view.setTemplateEngine(new TestTemplateEngine());
|
||||
view.setApplicationContext(this.webAppContext);
|
||||
|
||||
|
@ -109,35 +104,30 @@ public class GroovyMarkupViewTests {
|
|||
|
||||
@Test
|
||||
public void checkResource() throws Exception {
|
||||
|
||||
GroovyMarkupView view = createViewWithUrl("test.tpl");
|
||||
assertTrue(view.checkResource(Locale.US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMissingResource() throws Exception {
|
||||
|
||||
GroovyMarkupView view = createViewWithUrl("missing.tpl");
|
||||
assertFalse(view.checkResource(Locale.US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkI18nResource() throws Exception {
|
||||
|
||||
GroovyMarkupView view = createViewWithUrl("i18n.tpl");
|
||||
assertTrue(view.checkResource(Locale.FRENCH));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkI18nResourceMissingLocale() throws Exception {
|
||||
|
||||
GroovyMarkupView view = createViewWithUrl("i18n.tpl");
|
||||
assertTrue(view.checkResource(Locale.CHINESE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderMarkupTemplate() throws Exception {
|
||||
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("name", "Spring");
|
||||
MockHttpServletResponse response = renderViewWithModel("test.tpl", model, Locale.US);
|
||||
|
@ -155,7 +145,7 @@ public class GroovyMarkupViewTests {
|
|||
assertEquals("<p>Include German</p><p>Hallo Spring</p>", response.getContentAsString());
|
||||
|
||||
response = renderViewWithModel("i18n.tpl", model, new Locale("es"));
|
||||
assertEquals("<p>Include Default</p><p>¡hola Spring</p>", response.getContentAsString());
|
||||
assertEquals("<p>Include Default</p><p>Hola Spring</p>", response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -166,30 +156,30 @@ public class GroovyMarkupViewTests {
|
|||
response.getContentAsString());
|
||||
}
|
||||
|
||||
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model, Locale locale) throws Exception {
|
||||
|
||||
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model, Locale locale) throws Exception {
|
||||
GroovyMarkupView view = createViewWithUrl(viewUrl);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setPreferredLocales(Arrays.asList(locale));
|
||||
request.addPreferredLocale(locale);
|
||||
LocaleContextHolder.setLocale(locale);
|
||||
view.renderMergedTemplateModel(model, request, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
private GroovyMarkupView createViewWithUrl(String viewUrl) throws Exception {
|
||||
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
ctx.register(GroovyMarkupConfiguration.class);
|
||||
ctx.refresh();
|
||||
|
||||
GroovyMarkupView view = new GroovyMarkupView();
|
||||
view.setApplicationContext(ctx);
|
||||
view.setUrl(viewUrl);
|
||||
view.setApplicationContext(ctx);
|
||||
view.afterPropertiesSet();
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
public class TestTemplateEngine extends MarkupTemplateEngine {
|
||||
|
||||
public TestTemplateEngine() {
|
||||
|
@ -197,11 +187,12 @@ public class GroovyMarkupViewTests {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Template createTemplate(Reader reader) throws CompilationFailedException, ClassNotFoundException, IOException {
|
||||
public Template createTemplate(Reader reader) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class GroovyMarkupConfiguration {
|
||||
|
||||
|
@ -212,4 +203,5 @@ public class GroovyMarkupViewTests {
|
|||
return configurer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
include template: 'includes/include.tpl'
|
||||
p('¡hola Spring')
|
||||
p('Hola Spring')
|
Loading…
Reference in New Issue