Skip to content

Commit f988151

Browse files
committed
Improve charset management in XpathResultMatchers
Prior to this change, `XpathResultMatchers` and more generally the `MockHttpServletResponse` would default to ISO-8859-1 encoding even when it's not supposed to. The Servlet/HTTP specs mention this encoding for all `text/*` mime types when decoding bodies to Strings, but this issue is about XML Parsers. XML Parsers should use the encoding: * defined in the `Content-Type` response header (if available) * written in the XML declaration of the document * "guessed" by a built-in auto-detection mechanism This commit changes the following: * XPathMatchers now feed the XML parser with byte arrays instead of decoded Strings * the response should be written to `MockHttpServletResponse` using its OutputStream, and not a PrintWriter which defaults to ISO-8859-1 Issue: SPR-12676
1 parent a421bd2 commit f988151

File tree

5 files changed

+101
-60
lines changed

5 files changed

+101
-60
lines changed

spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ public boolean isWriterAccessAllowed() {
140140
return this.writerAccessAllowed;
141141
}
142142

143+
/**
144+
* Return whether the character encoding has been set.
145+
* <p>If {@code false}, {@link #getCharacterEncoding()} will return a default encoding value.
146+
*/
147+
public boolean isCharset() {
148+
return charset;
149+
}
150+
143151
@Override
144152
public void setCharacterEncoding(String characterEncoding) {
145153
this.characterEncoding = characterEncoding;

spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package org.springframework.test.util;
1818

19-
import java.io.StringReader;
19+
import static org.hamcrest.MatcherAssert.*;
20+
import static org.springframework.test.util.AssertionErrors.*;
21+
22+
import java.io.ByteArrayInputStream;
2023
import java.util.Collections;
2124
import java.util.Map;
25+
2226
import javax.xml.namespace.QName;
2327
import javax.xml.parsers.DocumentBuilder;
2428
import javax.xml.parsers.DocumentBuilderFactory;
@@ -35,11 +39,9 @@
3539
import org.xml.sax.InputSource;
3640

3741
import org.springframework.util.CollectionUtils;
42+
import org.springframework.util.StringUtils;
3843
import org.springframework.util.xml.SimpleNamespaceContext;
3944

40-
import static org.hamcrest.MatcherAssert.*;
41-
import static org.springframework.test.util.AssertionErrors.*;
42-
4345
/**
4446
* A helper class for applying assertions via XPath expressions.
4547
*
@@ -93,23 +95,27 @@ protected XPathExpression getXpathExpression() {
9395
* Parse the content, evaluate the XPath expression as a {@link Node}, and
9496
* assert it with the given {@code Matcher<Node>}.
9597
*/
96-
public void assertNode(String content, final Matcher<? super Node> matcher) throws Exception {
97-
Document document = parseXmlString(content);
98+
public void assertNode(byte[] content, String encoding, final Matcher<? super Node> matcher) throws Exception {
99+
Document document = parseXmlByteArray(content, encoding);
98100
Node node = evaluateXpath(document, XPathConstants.NODE, Node.class);
99101
assertThat("XPath " + this.expression, node, matcher);
100102
}
101103

102104
/**
103105
* Parse the given XML content to a {@link Document}.
104106
* @param xml the content to parse
107+
* @param encoding optional content encoding, if provided as metadata (e.g. in HTTP headers)
105108
* @return the parsed document
106-
* @throws Exception in case of errors
109+
* @throws Exception
107110
*/
108-
protected Document parseXmlString(String xml) throws Exception {
111+
protected Document parseXmlByteArray(byte[] xml, String encoding) throws Exception {
109112
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
110113
factory.setNamespaceAware(this.hasNamespaces);
111114
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
112-
InputSource inputSource = new InputSource(new StringReader(xml));
115+
InputSource inputSource = new InputSource(new ByteArrayInputStream(xml));
116+
if(StringUtils.hasText(encoding)) {
117+
inputSource.setEncoding(encoding);
118+
}
113119
return documentBuilder.parse(inputSource);
114120
}
115121

@@ -128,8 +134,8 @@ protected <T> T evaluateXpath(Document document, QName evaluationType, Class<T>
128134
* Apply the XPath expression and assert the resulting content exists.
129135
* @throws Exception if content parsing or expression evaluation fails
130136
*/
131-
public void exists(String content) throws Exception {
132-
Document document = parseXmlString(content);
137+
public void exists(byte[] content, String encoding) throws Exception {
138+
Document document = parseXmlByteArray(content, encoding);
133139
Node node = evaluateXpath(document, XPathConstants.NODE, Node.class);
134140
assertTrue("XPath " + this.expression + " does not exist", node != null);
135141
}
@@ -138,8 +144,8 @@ public void exists(String content) throws Exception {
138144
* Apply the XPath expression and assert the resulting content does not exist.
139145
* @throws Exception if content parsing or expression evaluation fails
140146
*/
141-
public void doesNotExist(String content) throws Exception {
142-
Document document = parseXmlString(content);
147+
public void doesNotExist(byte[] content, String encoding) throws Exception {
148+
Document document = parseXmlByteArray(content, encoding);
143149
Node node = evaluateXpath(document, XPathConstants.NODE, Node.class);
144150
assertTrue("XPath " + this.expression + " exists", node == null);
145151
}
@@ -149,8 +155,8 @@ public void doesNotExist(String content) throws Exception {
149155
* given Hamcrest matcher.
150156
* @throws Exception if content parsing or expression evaluation fails
151157
*/
152-
public void assertNodeCount(String content, Matcher<Integer> matcher) throws Exception {
153-
Document document = parseXmlString(content);
158+
public void assertNodeCount(byte[] content, String encoding, Matcher<Integer> matcher) throws Exception {
159+
Document document = parseXmlByteArray(content, encoding);
154160
NodeList nodeList = evaluateXpath(document, XPathConstants.NODESET, NodeList.class);
155161
assertThat("nodeCount for XPath " + this.expression, nodeList.getLength(), matcher);
156162
}
@@ -159,8 +165,8 @@ public void assertNodeCount(String content, Matcher<Integer> matcher) throws Exc
159165
* Apply the XPath expression and assert the resulting content as an integer.
160166
* @throws Exception if content parsing or expression evaluation fails
161167
*/
162-
public void assertNodeCount(String content, int expectedCount) throws Exception {
163-
Document document = parseXmlString(content);
168+
public void assertNodeCount(byte[] content, String encoding, int expectedCount) throws Exception {
169+
Document document = parseXmlByteArray(content, encoding);
164170
NodeList nodeList = evaluateXpath(document, XPathConstants.NODESET, NodeList.class);
165171
assertEquals("nodeCount for XPath " + this.expression, expectedCount, nodeList.getLength());
166172
}
@@ -170,8 +176,8 @@ public void assertNodeCount(String content, int expectedCount) throws Exception
170176
* given Hamcrest matcher.
171177
* @throws Exception if content parsing or expression evaluation fails
172178
*/
173-
public void assertString(String content, Matcher<? super String> matcher) throws Exception {
174-
Document document = parseXmlString(content);
179+
public void assertString(byte[] content, String encoding, Matcher<? super String> matcher) throws Exception {
180+
Document document = parseXmlByteArray(content, encoding);
175181
String result = evaluateXpath(document, XPathConstants.STRING, String.class);
176182
assertThat("XPath " + this.expression, result, matcher);
177183
}
@@ -180,8 +186,8 @@ public void assertString(String content, Matcher<? super String> matcher) throws
180186
* Apply the XPath expression and assert the resulting content as a String.
181187
* @throws Exception if content parsing or expression evaluation fails
182188
*/
183-
public void assertString(String content, String expectedValue) throws Exception {
184-
Document document = parseXmlString(content);
189+
public void assertString(byte[] content, String encoding, String expectedValue) throws Exception {
190+
Document document = parseXmlByteArray(content, encoding);
185191
String actual = evaluateXpath(document, XPathConstants.STRING, String.class);
186192
assertEquals("XPath " + this.expression, expectedValue, actual);
187193
}
@@ -191,8 +197,8 @@ public void assertString(String content, String expectedValue) throws Exception
191197
* given Hamcrest matcher.
192198
* @throws Exception if content parsing or expression evaluation fails
193199
*/
194-
public void assertNumber(String content, Matcher<? super Double> matcher) throws Exception {
195-
Document document = parseXmlString(content);
200+
public void assertNumber(byte[] content, String encoding, Matcher<? super Double> matcher) throws Exception {
201+
Document document = parseXmlByteArray(content, encoding);
196202
Double result = evaluateXpath(document, XPathConstants.NUMBER, Double.class);
197203
assertThat("XPath " + this.expression, result, matcher);
198204
}
@@ -201,8 +207,8 @@ public void assertNumber(String content, Matcher<? super Double> matcher) throws
201207
* Apply the XPath expression and assert the resulting content as a Double.
202208
* @throws Exception if content parsing or expression evaluation fails
203209
*/
204-
public void assertNumber(String content, Double expectedValue) throws Exception {
205-
Document document = parseXmlString(content);
210+
public void assertNumber(byte[] content, String encoding, Double expectedValue) throws Exception {
211+
Document document = parseXmlByteArray(content, encoding);
206212
Double actual = evaluateXpath(document, XPathConstants.NUMBER, Double.class);
207213
assertEquals("XPath " + this.expression, expectedValue, actual);
208214
}
@@ -211,8 +217,8 @@ public void assertNumber(String content, Double expectedValue) throws Exception
211217
* Apply the XPath expression and assert the resulting content as a Boolean.
212218
* @throws Exception if content parsing or expression evaluation fails
213219
*/
214-
public void assertBoolean(String content, boolean expectedValue) throws Exception {
215-
Document document = parseXmlString(content);
220+
public void assertBoolean(byte[] content, String encoding, boolean expectedValue) throws Exception {
221+
Document document = parseXmlByteArray(content, encoding);
216222
String actual = evaluateXpath(document, XPathConstants.STRING, String.class);
217223
assertEquals("XPath " + this.expression, expectedValue, Boolean.parseBoolean(actual));
218224
}

spring-test/src/main/java/org/springframework/test/web/client/match/XpathRequestMatchers.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,8 @@
3737
*/
3838
public class XpathRequestMatchers {
3939

40+
private static final String DEFAULT_ENCODING = "UTF-8";
41+
4042
private final XpathExpectationsHelper xpathHelper;
4143

4244

@@ -65,7 +67,7 @@ public <T> RequestMatcher node(final Matcher<? super Node> matcher) {
6567
return new AbstractXpathRequestMatcher() {
6668
@Override
6769
protected void matchInternal(MockClientHttpRequest request) throws Exception {
68-
xpathHelper.assertNode(request.getBodyAsString(), matcher);
70+
xpathHelper.assertNode(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
6971
}
7072
};
7173
}
@@ -77,7 +79,7 @@ public <T> RequestMatcher exists() {
7779
return new AbstractXpathRequestMatcher() {
7880
@Override
7981
protected void matchInternal(MockClientHttpRequest request) throws Exception {
80-
xpathHelper.exists(request.getBodyAsString());
82+
xpathHelper.exists(request.getBodyAsBytes(), DEFAULT_ENCODING);
8183
}
8284
};
8385
}
@@ -89,7 +91,7 @@ public <T> RequestMatcher doesNotExist() {
8991
return new AbstractXpathRequestMatcher() {
9092
@Override
9193
protected void matchInternal(MockClientHttpRequest request) throws Exception {
92-
xpathHelper.doesNotExist(request.getBodyAsString());
94+
xpathHelper.doesNotExist(request.getBodyAsBytes(), DEFAULT_ENCODING);
9395
}
9496
};
9597
}
@@ -102,7 +104,7 @@ public <T> RequestMatcher nodeCount(final Matcher<Integer> matcher) {
102104
return new AbstractXpathRequestMatcher() {
103105
@Override
104106
protected void matchInternal(MockClientHttpRequest request) throws Exception {
105-
xpathHelper.assertNodeCount(request.getBodyAsString(), matcher);
107+
xpathHelper.assertNodeCount(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
106108
}
107109
};
108110
}
@@ -114,7 +116,7 @@ public <T> RequestMatcher nodeCount(final int expectedCount) {
114116
return new AbstractXpathRequestMatcher() {
115117
@Override
116118
protected void matchInternal(MockClientHttpRequest request) throws Exception {
117-
xpathHelper.assertNodeCount(request.getBodyAsString(), expectedCount);
119+
xpathHelper.assertNodeCount(request.getBodyAsBytes(), DEFAULT_ENCODING, expectedCount);
118120
}
119121
};
120122
}
@@ -126,7 +128,7 @@ public <T> RequestMatcher string(final Matcher<? super String> matcher) {
126128
return new AbstractXpathRequestMatcher() {
127129
@Override
128130
protected void matchInternal(MockClientHttpRequest request) throws Exception {
129-
xpathHelper.assertString(request.getBodyAsString(), matcher);
131+
xpathHelper.assertString(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
130132
}
131133
};
132134
}
@@ -138,7 +140,7 @@ public RequestMatcher string(final String value) {
138140
return new AbstractXpathRequestMatcher() {
139141
@Override
140142
protected void matchInternal(MockClientHttpRequest request) throws Exception {
141-
xpathHelper.assertString(request.getBodyAsString(), value);
143+
xpathHelper.assertString(request.getBodyAsBytes(), DEFAULT_ENCODING, value);
142144
}
143145
};
144146
}
@@ -150,7 +152,7 @@ public <T> RequestMatcher number(final Matcher<? super Double> matcher) {
150152
return new AbstractXpathRequestMatcher() {
151153
@Override
152154
protected void matchInternal(MockClientHttpRequest request) throws Exception {
153-
xpathHelper.assertNumber(request.getBodyAsString(), matcher);
155+
xpathHelper.assertNumber(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
154156
}
155157
};
156158
}
@@ -162,7 +164,7 @@ public RequestMatcher number(final Double value) {
162164
return new AbstractXpathRequestMatcher() {
163165
@Override
164166
protected void matchInternal(MockClientHttpRequest request) throws Exception {
165-
xpathHelper.assertNumber(request.getBodyAsString(), value);
167+
xpathHelper.assertNumber(request.getBodyAsBytes(), DEFAULT_ENCODING, value);
166168
}
167169
};
168170
}
@@ -174,7 +176,7 @@ public <T> RequestMatcher booleanValue(final Boolean value) {
174176
return new AbstractXpathRequestMatcher() {
175177
@Override
176178
protected void matchInternal(MockClientHttpRequest request) throws Exception {
177-
xpathHelper.assertBoolean(request.getBodyAsString(), value);
179+
xpathHelper.assertBoolean(request.getBodyAsBytes(), DEFAULT_ENCODING, value);
178180
}
179181
};
180182
}

0 commit comments

Comments
 (0)