This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/master by this push: new 5e974d6 Fix BZ 64938 Clarify expected behaviour of setCharacterEncoding(null) 5e974d6 is described below commit 5e974d66cd4edcb9f18207d90b2ecd433104ec06 Author: Mark Thomas <ma...@apache.org> AuthorDate: Tue Feb 23 12:49:59 2021 +0000 Fix BZ 64938 Clarify expected behaviour of setCharacterEncoding(null) Also covers setContentType(null) and setLocale(null) https://bz.apache.org/bugzilla/show_bug.cgi?id=64938 --- .../apache/catalina/connector/OutputBuffer.java | 6 + java/org/apache/catalina/connector/Response.java | 40 ++- java/org/apache/coyote/Response.java | 9 +- .../apache/catalina/connector/TestResponse.java | 343 +++++++++++++++++++++ test/org/apache/tomcat/unittest/TesterContext.java | 23 +- webapps/docs/changelog.xml | 7 + 6 files changed, 408 insertions(+), 20 deletions(-) diff --git a/java/org/apache/catalina/connector/OutputBuffer.java b/java/org/apache/catalina/connector/OutputBuffer.java index 59939ef..ac47555 100644 --- a/java/org/apache/catalina/connector/OutputBuffer.java +++ b/java/org/apache/catalina/connector/OutputBuffer.java @@ -35,6 +35,7 @@ import org.apache.catalina.Globals; import org.apache.coyote.ActionCode; import org.apache.coyote.CloseNowException; import org.apache.coyote.Response; +import org.apache.tomcat.util.buf.B2CConverter; import org.apache.tomcat.util.buf.C2BConverter; import org.apache.tomcat.util.res.StringManager; @@ -560,6 +561,11 @@ public class OutputBuffer extends Writer { } if (charset == null) { + if (coyoteResponse.getCharacterEncoding() != null) { + // setCharacterEncoding() was called with an invalid character set + // Trigger an UnsupportedEncodingException + charset = B2CConverter.getCharset(coyoteResponse.getCharacterEncoding()); + } charset = org.apache.coyote.Constants.DEFAULT_BODY_CHARSET; } diff --git a/java/org/apache/catalina/connector/Response.java b/java/org/apache/catalina/connector/Response.java index 783772a..d6c0e92 100644 --- a/java/org/apache/catalina/connector/Response.java +++ b/java/org/apache/catalina/connector/Response.java @@ -724,6 +724,12 @@ public class Response implements HttpServletResponse { if (type == null) { getCoyoteResponse().setContentType(null); + try { + getCoyoteResponse().setCharacterEncoding(null); + } catch (UnsupportedEncodingException e) { + // Can never happen when calling with null + } + isCharacterEncodingSet = false; return; } @@ -791,7 +797,11 @@ public class Response implements HttpServletResponse { log.warn(sm.getString("coyoteResponse.encoding.invalid", charset), e); return; } - isCharacterEncodingSet = true; + if (charset == null) { + isCharacterEncodingSet = false; + } else { + isCharacterEncodingSet = true; + } } @@ -825,16 +835,24 @@ public class Response implements HttpServletResponse { return; } - // In some error handling scenarios, the context is unknown - // (e.g. a 404 when a ROOT context is not present) - Context context = getContext(); - if (context != null) { - String charset = context.getCharset(locale); - if (charset != null) { - try { - getCoyoteResponse().setCharacterEncoding(charset); - } catch (UnsupportedEncodingException e) { - log.warn(sm.getString("coyoteResponse.encoding.invalid", charset), e); + if (locale == null) { + try { + getCoyoteResponse().setCharacterEncoding(null); + } catch (UnsupportedEncodingException e) { + // Impossible when calling with null + } + } else { + // In some error handling scenarios, the context is unknown + // (e.g. a 404 when a ROOT context is not present) + Context context = getContext(); + if (context != null) { + String charset = context.getCharset(locale); + if (charset != null) { + try { + getCoyoteResponse().setCharacterEncoding(charset); + } catch (UnsupportedEncodingException e) { + log.warn(sm.getString("coyoteResponse.encoding.invalid", charset), e); + } } } } diff --git a/java/org/apache/coyote/Response.java b/java/org/apache/coyote/Response.java index 91473a1..d5f95ea 100644 --- a/java/org/apache/coyote/Response.java +++ b/java/org/apache/coyote/Response.java @@ -465,8 +465,9 @@ public final class Response { public void setLocale(Locale locale) { if (locale == null) { - return; // throw an exception? - } + this.locale = null; + this.contentLanguage = null; + return; } // Save the locale for use by getLocale() this.locale = locale; @@ -500,11 +501,13 @@ public final class Response { return; } if (characterEncoding == null) { + this.charset = null; + this.characterEncoding = null; return; } - this.charset = B2CConverter.getCharset(characterEncoding); this.characterEncoding = characterEncoding; + this.charset = B2CConverter.getCharset(characterEncoding); } diff --git a/test/org/apache/catalina/connector/TestResponse.java b/test/org/apache/catalina/connector/TestResponse.java index 55fb014..c24bc44 100644 --- a/test/org/apache/catalina/connector/TestResponse.java +++ b/test/org/apache/catalina/connector/TestResponse.java @@ -19,7 +19,10 @@ package org.apache.catalina.connector; import java.io.IOException; import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Locale; import java.util.Map; import jakarta.servlet.ServletException; @@ -636,6 +639,346 @@ public class TestResponse extends TomcatBaseTest { } + private static final String ISO_8859_1 = StandardCharsets.ISO_8859_1.name(); + private static final String UTF_8 = StandardCharsets.UTF_8.name(); + private static final String UNKNOWN = "unknown"; + private static final String TEXT = "text/plain"; + private static final String TEXT_ISO_8859_1 = TEXT + ";charset=" + ISO_8859_1; + private static final String TEXT_UTF_8 = TEXT + ";charset=" + UTF_8; + private static final String TEXT_UNKNOWN = TEXT + ";charset=" + UNKNOWN; + private static final Locale UNDETERMINED = new Locale("und"); + + @Test + public void testSetCharacterEncoding01() { + Response response = setupResponse(); + + // Check default + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding02() { + Response response = setupResponse(); + + // Check multiple calls + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding03() throws IOException { + Response response = setupResponse(); + + // Check after getWriter() + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.getWriter(); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding04() throws IOException { + Response response = setupResponse(); + + // Check after commit + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.flushBuffer(); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding05() { + Response response = setupResponse(); + + // Check calling with null + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test(expected = UnsupportedEncodingException.class) + public void testSetCharacterEncoding06() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set and writer + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getWriter(); + } + + + @Test + public void testSetCharacterEncoding07() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getOutputStream(); + } + + + @Test + public void testSetCharacterEncoding08() { + Response response = setupResponse(); + + // Check multiple calls with different methods + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType01() { + Response response = setupResponse(); + + // Check multiple calls + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType02() throws IOException { + Response response = setupResponse(); + + // Check after getWriter() + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.getWriter(); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType03() throws IOException { + Response response = setupResponse(); + + // Check after commit + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.flushBuffer(); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType04() { + Response response = setupResponse(); + + // Check calling with null + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test(expected = UnsupportedEncodingException.class) + public void testSetContentType05() throws IOException { + Response response = setupResponse(); + response.getContext().addLocaleEncodingMappingParameter(Locale.UK.toLanguageTag(), UNKNOWN); + + // Check calling with an unknown character set and writer + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getWriter(); + } + + + @Test + public void testSetContentType06() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getOutputStream(); + } + + + @Test + public void testSetLocale01() { + Response response = setupResponse(); + + // Check multiple calls + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale02() throws IOException { + Response response = setupResponse(); + + // Check after getWriter() + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.getWriter(); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale03() throws IOException { + Response response = setupResponse(); + + // Check after commit + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.flushBuffer(); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale04() { + Response response = setupResponse(); + + // Check calling with null + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test(expected = UnsupportedEncodingException.class) + public void testSetLocale05() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set and writer + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(UNDETERMINED); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getWriter(); + } + + + @Test + public void testSetLocale06() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(UNDETERMINED); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getOutputStream(); + } + + + @Test + public void testSetLocale07() { + Response response = setupResponse(); + + // Check setLocale() is over-ridden by setCharacterEncoding + + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale doesn't change previous value + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + + // Reset + response.setCharacterEncoding(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale is over-ridden by setCharacterEncoding + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale08() { + Response response = setupResponse(); + + // Check setLocale() is over-ridden by setContentType + + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale doesn't change previous value + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + + // Reset + response.setContentType(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale is over-ridden by setContentTpe + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + private Response setupResponse() { + Connector connector = new Connector(); + org.apache.coyote.Response cResponse = new org.apache.coyote.Response(); + Response response = new Response(); + response.setCoyoteResponse(cResponse); + Request request = new Request(connector); + org.apache.coyote.Request cRequest = new org.apache.coyote.Request(); + request.setCoyoteRequest(cRequest); + Context context = new TesterContext(); + request.getMappingData().context = context; + response.setRequest(request); + context.addLocaleEncodingMappingParameter(Locale.ENGLISH.getLanguage(), ISO_8859_1); + context.addLocaleEncodingMappingParameter(Locale.CHINESE.getLanguage(), UTF_8); + context.addLocaleEncodingMappingParameter(UNDETERMINED.toLanguageTag(), UNKNOWN); + return response; + } + + private static final class Bug52811Servlet extends HttpServlet { private static final long serialVersionUID = 1L; diff --git a/test/org/apache/tomcat/unittest/TesterContext.java b/test/org/apache/tomcat/unittest/TesterContext.java index 4e0e362..7164bcc 100644 --- a/test/org/apache/tomcat/unittest/TesterContext.java +++ b/test/org/apache/tomcat/unittest/TesterContext.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.management.ObjectName; @@ -365,11 +366,6 @@ public class TesterContext implements Context { } @Override - public String getCharset(Locale locale) { - return null; - } - - @Override public URL getConfigFile() { return null; } @@ -739,9 +735,24 @@ public class TesterContext implements Context { // NO-OP } + private final Map<String,String> localEncodingMap = new ConcurrentHashMap<>(); + @Override public void addLocaleEncodingMappingParameter(String locale, String encoding) { - // NO-OP + localEncodingMap.put(locale, encoding); + } + @Override + public String getCharset(Locale locale) { + // Match full language_country_variant first, then language_country, + // then language only + String charset = localEncodingMap.get(locale.toString()); + if (charset == null) { + charset = localEncodingMap.get(locale.getLanguage() + "_" + locale.getCountry()); + if (charset == null) { + charset = localEncodingMap.get(locale.getLanguage()); + } + } + return charset; } @Override diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 42ffdbd..f638141 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -134,6 +134,13 @@ <code>webapps</code> where it will be deployed (or not) based on the current settings for automatic deployment. (markt) </add> + <fix> + <bug>64938</bug>: Align the behaviour when <code>null</code> is passed + to the <code>ServletResponse</code> methods + <code>setCharacterEncoding()</code>, <code>setContentType()</code> and + <code>setLocale()</code> with the recent clarification from the Jakarta + Servlet project of the expected behaviour in these cases. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org