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:
Juergen Hoeller 2016-08-26 11:14:02 +02:00
parent bbd5993945
commit a8f7f75f64
8 changed files with 96 additions and 114 deletions

View File

@ -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"
}

View File

@ -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 == '~';
}
}

View File

@ -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);
}
}

View File

@ -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 == '~';
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -1,2 +1,2 @@
include template: 'includes/include.tpl'
p('¡hola Spring')
p('Hola Spring')