diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 48e62c789e3..d7ce67d4809 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -285,7 +285,15 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } } - private static Charset getCharset(@Nullable MediaType contentType) { + /** + * Return the charset to use for JSON input. + *

By default this is either the charset from the input {@code MediaType} + * or otherwise falling back on {@code UTF-8}. + * @param contentType the content type of the HTTP input message + * @return the charset to use + * @since 5.1.18 + */ + protected static Charset getCharset(@Nullable MediaType contentType) { if (contentType != null && contentType.getCharset() != null) { return contentType.getCharset(); } diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java index dac3ab709f1..40d74ae24f7 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -81,20 +81,11 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { } - private final UrlPathHelper pathHelper; - private boolean removeOnly; private boolean relativeRedirects; - public ForwardedHeaderFilter() { - this.pathHelper = new UrlPathHelper(); - this.pathHelper.setUrlDecode(false); - this.pathHelper.setRemoveSemicolonContent(false); - } - - /** * Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are * removed only and the information in them ignored. @@ -151,7 +142,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { } else { HttpServletRequest wrappedRequest = - new ForwardedHeaderExtractingRequest(request, this.pathHelper); + new ForwardedHeaderExtractingRequest(request); HttpServletResponse wrappedResponse = this.relativeRedirects ? RelativeRedirectResponseWrapper.wrapIfNecessary(response, HttpStatus.SEE_OTHER) : @@ -235,7 +226,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { private final ForwardedPrefixExtractor forwardedPrefixExtractor; - ForwardedHeaderExtractingRequest(HttpServletRequest servletRequest, UrlPathHelper pathHelper) { + ForwardedHeaderExtractingRequest(HttpServletRequest servletRequest) { super(servletRequest); ServerHttpRequest request = new ServletServerHttpRequest(servletRequest); @@ -251,7 +242,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { String baseUrl = this.scheme + "://" + this.host + (port == -1 ? "" : ":" + port); Supplier delegateRequest = () -> (HttpServletRequest) getRequest(); - this.forwardedPrefixExtractor = new ForwardedPrefixExtractor(delegateRequest, pathHelper, baseUrl); + this.forwardedPrefixExtractor = new ForwardedPrefixExtractor(delegateRequest, baseUrl); } @@ -320,8 +311,6 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { private final Supplier delegate; - private final UrlPathHelper pathHelper; - private final String baseUrl; private String actualRequestUri; @@ -340,14 +329,10 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { * @param delegateRequest supplier for the current * {@link HttpServletRequestWrapper#getRequest() delegate request} which * may change during a forward (e.g. Tomcat. - * @param pathHelper the path helper instance * @param baseUrl the host, scheme, and port based on forwarded headers */ - public ForwardedPrefixExtractor( - Supplier delegateRequest, UrlPathHelper pathHelper, String baseUrl) { - + public ForwardedPrefixExtractor(Supplier delegateRequest, String baseUrl) { this.delegate = delegateRequest; - this.pathHelper = pathHelper; this.baseUrl = baseUrl; this.actualRequestUri = delegateRequest.get().getRequestURI(); @@ -384,7 +369,8 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { @Nullable private String initRequestUri() { if (this.forwardedPrefix != null) { - return this.forwardedPrefix + this.pathHelper.getPathWithinApplication(this.delegate.get()); + return this.forwardedPrefix + + UrlPathHelper.rawPathInstance.getPathWithinApplication(this.delegate.get()); } return null; } diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index cf93e43567d..2988c4628de 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -192,7 +192,13 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { } /** - * Create a builder that is initialized with the given {@code URI}. + * Create a builder that is initialized from the given {@code URI}. + *

Note: the components in the resulting builder will be + * in fully encoded (raw) form and further changes must also supply values + * that are fully encoded, for example via methods in {@link UriUtils}. + * In addition please use {@link #build(boolean)} with a value of "true" to + * build the {@link UriComponents} instance in order to indicate that the + * components are encoded. * @param uri the URI to initialize with * @return the new {@code UriComponentsBuilder} */ @@ -439,11 +445,13 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { } /** - * Build a {@code UriComponents} instance from the various components - * contained in this builder. - * @param encoded whether all the components set in this builder are - * encoded ({@code true}) or not ({@code false}) + * Variant of {@link #build()} to create a {@link UriComponents} instance + * when components are already fully encoded. This is useful for example if + * the builder was created via {@link UriComponentsBuilder#fromUri(URI)}. + * @param encoded whether the components in this builder are already encoded * @return the URI components + * @throws IllegalArgumentException if any of the components contain illegal + * characters that should have been encoded. */ public UriComponents build(boolean encoded) { return buildInternal(encoded ? diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 96ddf07a51e..fbeb51f3fe5 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -85,6 +85,8 @@ public class UrlPathHelper { private String defaultEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; + private boolean readOnly = false; + /** * Whether URL lookups should always use the full path within the current @@ -96,6 +98,7 @@ public class UrlPathHelper { *

By default this is set to "false". */ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + checkReadOnly(); this.alwaysUseFullPath = alwaysUseFullPath; } @@ -118,6 +121,7 @@ public class UrlPathHelper { * @see java.net.URLDecoder#decode(String, String) */ public void setUrlDecode(boolean urlDecode) { + checkReadOnly(); this.urlDecode = urlDecode; } @@ -134,6 +138,7 @@ public class UrlPathHelper { *

Default is "true". */ public void setRemoveSemicolonContent(boolean removeSemicolonContent) { + checkReadOnly(); this.removeSemicolonContent = removeSemicolonContent; } @@ -141,6 +146,7 @@ public class UrlPathHelper { * Whether configured to remove ";" (semicolon) content from the request URI. */ public boolean shouldRemoveSemicolonContent() { + checkReadOnly(); return this.removeSemicolonContent; } @@ -158,6 +164,7 @@ public class UrlPathHelper { * @see WebUtils#DEFAULT_CHARACTER_ENCODING */ public void setDefaultEncoding(String defaultEncoding) { + checkReadOnly(); this.defaultEncoding = defaultEncoding; } @@ -168,6 +175,17 @@ public class UrlPathHelper { return this.defaultEncoding; } + /** + * Switch to read-only mode where further configuration changes are not allowed. + */ + private void setReadOnly() { + this.readOnly = true; + } + + private void checkReadOnly() { + Assert.isTrue(!this.readOnly, "This instance cannot be modified"); + } + /** * {@link #getLookupPathForRequest Resolve} the lookupPath and cache it in a @@ -590,8 +608,7 @@ public class UrlPathHelper { * @return the updated URI string */ public String removeSemicolonContent(String requestUri) { - return (this.removeSemicolonContent ? - removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri)); + return (this.removeSemicolonContent ? removeSemicolonContentInternal(requestUri) : requestUri); } private String removeSemicolonContentInternal(String requestUri) { @@ -605,16 +622,6 @@ public class UrlPathHelper { return requestUri; } - private String removeJsessionid(String requestUri) { - int startIndex = requestUri.toLowerCase().indexOf(";jsessionid="); - if (startIndex != -1) { - int endIndex = requestUri.indexOf(';', startIndex + 12); - String start = requestUri.substring(0, startIndex); - requestUri = (endIndex != -1) ? start + requestUri.substring(endIndex) : start; - } - return requestUri; - } - /** * Decode the given URI path variables via {@link #decodeRequestString} unless * {@link #setUrlDecode} is set to {@code true} in which case it is assumed @@ -695,7 +702,7 @@ public class UrlPathHelper { /** - * Shared, read-only instance of {@code UrlPathHelper}. Uses default settings: + * Shared, read-only instance with defaults. The following apply: *

*/ - public static final UrlPathHelper defaultInstance = new UrlPathHelper() { + public static final UrlPathHelper defaultInstance = new UrlPathHelper(); - @Override - public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { - throw new UnsupportedOperationException(); - } + static { + defaultInstance.setReadOnly(); + } - @Override - public void setUrlDecode(boolean urlDecode) { - throw new UnsupportedOperationException(); - } - @Override - public void setRemoveSemicolonContent(boolean removeSemicolonContent) { - throw new UnsupportedOperationException(); - } + /** + * Shared, read-only instance for the full, encoded path. The following apply: + * + */ + public static final UrlPathHelper rawPathInstance = new UrlPathHelper(); - @Override - public void setDefaultEncoding(String defaultEncoding) { - throw new UnsupportedOperationException(); - } - }; + static { + rawPathInstance.setAlwaysUseFullPath(true); + rawPathInstance.setUrlDecode(false); + rawPathInstance.setRemoveSemicolonContent(false); + rawPathInstance.setReadOnly(); + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java index fec0cfdbe87..8a0e76127ae 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -733,6 +733,9 @@ public abstract class WebUtils { int index = pair.indexOf('='); if (index != -1) { String name = pair.substring(0, index); + if (name.equalsIgnoreCase("jsessionid")) { + continue; + } String rawValue = pair.substring(index + 1); for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) { result.add(name, value); diff --git a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java index ee53ff82079..9bd57cb1bab 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java @@ -126,14 +126,7 @@ public class UrlPathHelperTests { assertThat(helper.getRequestUri(request)).isEqualTo("/foo;a=b;c=d"); request.setRequestURI("/foo;jsessionid=c0o7fszeb1"); - assertThat(helper.getRequestUri(request)).as("jsessionid should always be removed").isEqualTo("/foo"); - - request.setRequestURI("/foo;a=b;jsessionid=c0o7fszeb1;c=d"); - assertThat(helper.getRequestUri(request)).as("jsessionid should always be removed").isEqualTo("/foo;a=b;c=d"); - - // SPR-10398 - request.setRequestURI("/foo;a=b;JSESSIONID=c0o7fszeb1;c=d"); - assertThat(helper.getRequestUri(request)).as("JSESSIONID should always be removed").isEqualTo("/foo;a=b;c=d"); + assertThat(helper.getRequestUri(request)).isEqualTo("/foo;jsessionid=c0o7fszeb1"); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java index 68226877b16..998af1c9eb1 100644 --- a/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -16,7 +16,6 @@ package org.springframework.web.util; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -65,29 +64,42 @@ public class WebUtilsTests { MultiValueMap variables; variables = WebUtils.parseMatrixVariables(null); - assertThat(variables.size()).isEqualTo(0); + assertThat(variables).hasSize(0); variables = WebUtils.parseMatrixVariables("year"); - assertThat(variables.size()).isEqualTo(1); + assertThat(variables).hasSize(1); assertThat(variables.getFirst("year")).isEqualTo(""); variables = WebUtils.parseMatrixVariables("year=2012"); - assertThat(variables.size()).isEqualTo(1); + assertThat(variables).hasSize(1); assertThat(variables.getFirst("year")).isEqualTo("2012"); variables = WebUtils.parseMatrixVariables("year=2012;colors=red,blue,green"); - assertThat(variables.size()).isEqualTo(2); - assertThat(variables.get("colors")).isEqualTo(Arrays.asList("red", "blue", "green")); + assertThat(variables).hasSize(2); + assertThat(variables.get("colors")).containsExactly("red", "blue", "green"); assertThat(variables.getFirst("year")).isEqualTo("2012"); variables = WebUtils.parseMatrixVariables(";year=2012;colors=red,blue,green;"); - assertThat(variables.size()).isEqualTo(2); - assertThat(variables.get("colors")).isEqualTo(Arrays.asList("red", "blue", "green")); + assertThat(variables).hasSize(2); + assertThat(variables.get("colors")).containsExactly("red", "blue", "green"); assertThat(variables.getFirst("year")).isEqualTo("2012"); variables = WebUtils.parseMatrixVariables("colors=red;colors=blue;colors=green"); - assertThat(variables.size()).isEqualTo(1); - assertThat(variables.get("colors")).isEqualTo(Arrays.asList("red", "blue", "green")); + assertThat(variables).hasSize(1); + assertThat(variables.get("colors")).containsExactly("red", "blue", "green"); + + variables = WebUtils.parseMatrixVariables("jsessionid=c0o7fszeb1"); + assertThat(variables).isEmpty(); + + variables = WebUtils.parseMatrixVariables("a=b;jsessionid=c0o7fszeb1;c=d"); + assertThat(variables).hasSize(2); + assertThat(variables.get("a")).containsExactly("b"); + assertThat(variables.get("c")).containsExactly("d"); + + variables = WebUtils.parseMatrixVariables("a=b;jsessionid=c0o7fszeb1;c=d"); + assertThat(variables).hasSize(2); + assertThat(variables.get("a")).containsExactly("b"); + assertThat(variables.get("c")).containsExactly("d"); } @Test diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 4508da9f215..932ff78a057 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -91,16 +91,6 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe new ParameterizedTypeReference>() { }.getType(); - private static final UrlPathHelper decodingUrlPathHelper = UrlPathHelper.defaultInstance; - - private static final UrlPathHelper rawUrlPathHelper = new UrlPathHelper(); - - static { - rawUrlPathHelper.setRemoveSemicolonContent(false); - rawUrlPathHelper.setUrlDecode(false); - } - - private final ContentNegotiationManager contentNegotiationManager; private final Set safeExtensions = new HashSet<>(); @@ -430,7 +420,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } HttpServletRequest servletRequest = request.getServletRequest(); - String requestUri = rawUrlPathHelper.getOriginatingRequestUri(servletRequest); + String requestUri = UrlPathHelper.rawPathInstance.getOriginatingRequestUri(servletRequest); int index = requestUri.lastIndexOf('/') + 1; String filename = requestUri.substring(index); @@ -442,10 +432,10 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe filename = filename.substring(0, index); } - filename = decodingUrlPathHelper.decodeRequestString(servletRequest, filename); + filename = UrlPathHelper.defaultInstance.decodeRequestString(servletRequest, filename); String ext = StringUtils.getFilenameExtension(filename); - pathParams = decodingUrlPathHelper.decodeRequestString(servletRequest, pathParams); + pathParams = UrlPathHelper.defaultInstance.decodeRequestString(servletRequest, pathParams); String extInPathParams = StringUtils.getFilenameExtension(pathParams); if (!safeExtension(servletRequest, ext) || !safeExtension(servletRequest, extInPathParams)) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java index 243f26117c6..900e224f195 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java index 40a19881077..272700b49a4 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -34,6 +34,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; @@ -270,6 +271,7 @@ public class TransportHandlingSockJsService extends AbstractSockJsService implem } SockJsSession session = this.sessions.get(sessionId); + boolean isNewSession = false; if (session == null) { if (transportHandler instanceof SockJsSessionFactory) { Map attributes = new HashMap<>(); @@ -278,6 +280,7 @@ public class TransportHandlingSockJsService extends AbstractSockJsService implem } SockJsSessionFactory sessionFactory = (SockJsSessionFactory) transportHandler; session = createSockJsSession(sessionId, sessionFactory, handler, attributes); + isNewSession = true; } else { response.setStatusCode(HttpStatus.NOT_FOUND); @@ -311,6 +314,14 @@ public class TransportHandlingSockJsService extends AbstractSockJsService implem } transportHandler.handleRequest(request, response, handler, session); + + if (isNewSession && (response instanceof ServletServerHttpResponse)) { + int status = ((ServletServerHttpResponse) response).getServletResponse().getStatus(); + if (HttpStatus.valueOf(status).is4xxClientError()) { + this.sessions.remove(sessionId); + } + } + chain.applyAfterHandshake(request, response, null); } catch (SockJsException ex) { diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index c976de30b3c..250626cb82e 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -3381,6 +3381,39 @@ which allow rendering only a subset of all fields in an `Object`. To use it with NOTE: `@JsonView` allows an array of view classes, but you can specify only one per controller method. If you need to activate multiple views, you can use a composite interface. +If you want to do the above programmatically, instead of declaring an `@JsonView` annotation, +wrap the return value with `MappingJacksonValue` and use it to supply the serialization view: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @RestController + public class UserController { + + @GetMapping("/user") + public MappingJacksonValue getUser() { + User user = new User("eric", "7!jd#h23"); + MappingJacksonValue value = new MappingJacksonValue(user); + value.setSerializationView(User.WithoutPasswordView.class); + return value; + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @RestController + class UserController { + + @GetMapping("/user") + fun getUser(): MappingJacksonValue { + val value = MappingJacksonValue(User("eric", "7!jd#h23")) + value.serializationView = User.WithoutPasswordView::class.java + return value + } + } +---- + For controllers that rely on view resolution, you can add the serialization view class to the model, as the following example shows: