Skip to content

Commit 02da848

Browse files
committed
Merge branch '5.1.x'
2 parents e905f17 + 57558a4 commit 02da848

File tree

11 files changed

+254
-46
lines changed

11 files changed

+254
-46
lines changed

spring-web/src/main/java/org/springframework/http/server/PathContainer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
public interface PathContainer {
3737

3838
/**
39-
* The original path that was parsed.
39+
* The original (raw, encoded) path that this instance was parsed from.
4040
*/
4141
String value();
4242

@@ -83,7 +83,7 @@ static PathContainer parsePath(String path) {
8383
interface Element {
8484

8585
/**
86-
* Return the original, raw (encoded) value for the path component.
86+
* Return the original (raw, encoded) value of this path element.
8787
*/
8888
String value();
8989
}

spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.io.UnsupportedEncodingException;
2121
import java.net.URLDecoder;
22+
import java.nio.charset.StandardCharsets;
2223
import java.util.Arrays;
2324
import java.util.List;
2425

@@ -31,6 +32,7 @@
3132
import org.springframework.lang.Nullable;
3233
import org.springframework.util.StringUtils;
3334
import org.springframework.web.server.ServerWebExchange;
35+
import org.springframework.web.util.UriUtils;
3436

3537
/**
3638
* A simple {@code ResourceResolver} that tries to find a resource under the given
@@ -108,6 +110,9 @@ private Mono<Resource> getResource(String resourcePath, List<? extends Resource>
108110
*/
109111
protected Mono<Resource> getResource(String resourcePath, Resource location) {
110112
try {
113+
if (location instanceof ClassPathResource) {
114+
resourcePath = UriUtils.decode(resourcePath, StandardCharsets.UTF_8);
115+
}
111116
Resource resource = location.createRelative(resourcePath);
112117
if (resource.isReadable()) {
113118
if (checkResource(resource, location)) {

spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceResolver.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -21,6 +21,7 @@
2121
import reactor.core.publisher.Mono;
2222

2323
import org.springframework.core.io.Resource;
24+
import org.springframework.http.server.RequestPath;
2425
import org.springframework.lang.Nullable;
2526
import org.springframework.web.server.ServerWebExchange;
2627

@@ -40,7 +41,8 @@ public interface ResourceResolver {
4041
* Resolve the supplied request and request path to a {@link Resource} that
4142
* exists under one of the given resource locations.
4243
* @param exchange the current exchange
43-
* @param requestPath the portion of the request path to use
44+
* @param requestPath the portion of the request path to use. This is
45+
* expected to be the encoded path, i.e. {@link RequestPath#value()}.
4446
* @param locations the locations to search in when looking up resources
4547
* @param chain the chain of remaining resolvers to delegate to
4648
* @return the resolved resource or an empty {@code Mono} if unresolved
@@ -53,7 +55,8 @@ Mono<Resource> resolveResource(@Nullable ServerWebExchange exchange, String requ
5355
* to access the resource that is located at the given <em>internal</em>
5456
* resource path.
5557
* <p>This is useful when rendering URL links to clients.
56-
* @param resourcePath the internal resource path
58+
* @param resourcePath the "internal" resource path to resolve a path for
59+
* public use. This is expected to be the encoded path.
5760
* @param locations the locations to search in when looking up resources
5861
* @param chain the chain of resolvers to delegate to
5962
* @return the resolved public URL path or an empty {@code Mono} if unresolved

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.core.codec.DecodingException;
3636
import org.springframework.core.codec.Hints;
3737
import org.springframework.core.io.buffer.DataBuffer;
38+
import org.springframework.core.io.buffer.DataBufferUtils;
3839
import org.springframework.http.HttpMethod;
3940
import org.springframework.http.MediaType;
4041
import org.springframework.http.codec.HttpMessageReader;
@@ -205,7 +206,8 @@ protected Mono<Object> readBody(MethodParameter bodyParam, @Nullable MethodParam
205206

206207
HttpMethod method = request.getMethod();
207208
if (contentType == null && method != null && SUPPORTED_METHODS.contains(method)) {
208-
Flux<DataBuffer> body = request.getBody().doOnNext(o -> {
209+
Flux<DataBuffer> body = request.getBody().doOnNext(buffer -> {
210+
DataBufferUtils.release(buffer);
209211
// Body not empty, back to 415..
210212
throw new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes, elementType);
211213
});

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -32,7 +32,6 @@
3232
import freemarker.template.SimpleHash;
3333
import freemarker.template.Template;
3434
import freemarker.template.Version;
35-
import reactor.core.publisher.Flux;
3635
import reactor.core.publisher.Mono;
3736

3837
import org.springframework.beans.BeansException;
@@ -42,6 +41,7 @@
4241
import org.springframework.context.i18n.LocaleContextHolder;
4342
import org.springframework.core.io.buffer.DataBuffer;
4443
import org.springframework.core.io.buffer.DataBufferUtils;
44+
import org.springframework.core.io.buffer.PooledDataBuffer;
4545
import org.springframework.http.MediaType;
4646
import org.springframework.lang.Nullable;
4747
import org.springframework.util.Assert;
@@ -184,30 +184,34 @@ public boolean checkResourceExists(Locale locale) throws Exception {
184184
protected Mono<Void> renderInternal(Map<String, Object> renderAttributes,
185185
@Nullable MediaType contentType, ServerWebExchange exchange) {
186186

187-
// Expose all standard FreeMarker hash models.
188-
SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
187+
return exchange.getResponse().writeWith(Mono
188+
.fromCallable(() -> {
189+
// Expose all standard FreeMarker hash models.
190+
SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
189191

190-
if (logger.isDebugEnabled()) {
191-
logger.debug(exchange.getLogPrefix() + "Rendering [" + getUrl() + "]");
192-
}
192+
if (logger.isDebugEnabled()) {
193+
logger.debug(exchange.getLogPrefix() + "Rendering [" + getUrl() + "]");
194+
}
193195

194-
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
195-
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
196-
try {
197-
Charset charset = getCharset(contentType);
198-
Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset);
199-
getTemplate(locale).process(freeMarkerModel, writer);
200-
}
201-
catch (IOException ex) {
202-
DataBufferUtils.release(dataBuffer);
203-
String message = "Could not load FreeMarker template for URL [" + getUrl() + "]";
204-
return Mono.error(new IllegalStateException(message, ex));
205-
}
206-
catch (Throwable ex) {
207-
DataBufferUtils.release(dataBuffer);
208-
return Mono.error(ex);
209-
}
210-
return exchange.getResponse().writeWith(Flux.just(dataBuffer));
196+
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
197+
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
198+
try {
199+
Charset charset = getCharset(contentType);
200+
Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset);
201+
getTemplate(locale).process(freeMarkerModel, writer);
202+
return dataBuffer;
203+
}
204+
catch (IOException ex) {
205+
DataBufferUtils.release(dataBuffer);
206+
String message = "Could not load FreeMarker template for URL [" + getUrl() + "]";
207+
throw new IllegalStateException(message, ex);
208+
}
209+
catch (Throwable ex) {
210+
DataBufferUtils.release(dataBuffer);
211+
throw ex;
212+
}
213+
})
214+
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release));
211215
}
212216

213217
private Charset getCharset(@Nullable MediaType mediaType) {

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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,9 +37,7 @@
3737
import org.springframework.context.ApplicationContextException;
3838
import org.springframework.context.i18n.LocaleContextHolder;
3939
import org.springframework.core.io.Resource;
40-
import org.springframework.core.io.buffer.DataBuffer;
4140
import org.springframework.http.MediaType;
42-
import org.springframework.http.server.reactive.ServerHttpResponse;
4341
import org.springframework.lang.Nullable;
4442
import org.springframework.scripting.support.StandardScriptEvalException;
4543
import org.springframework.scripting.support.StandardScriptUtils;
@@ -301,8 +299,7 @@ public boolean checkResourceExists(Locale locale) throws Exception {
301299
protected Mono<Void> renderInternal(
302300
Map<String, Object> model, @Nullable MediaType contentType, ServerWebExchange exchange) {
303301

304-
return Mono.defer(() -> {
305-
ServerHttpResponse response = exchange.getResponse();
302+
return exchange.getResponse().writeWith(Mono.fromCallable(() -> {
306303
try {
307304
ScriptEngine engine = getEngine();
308305
String url = getUrl();
@@ -338,16 +335,15 @@ else if (this.renderObject != null) {
338335
}
339336

340337
byte[] bytes = String.valueOf(html).getBytes(StandardCharsets.UTF_8);
341-
DataBuffer buffer = response.bufferFactory().allocateBuffer(bytes.length).write(bytes);
342-
return response.writeWith(Mono.just(buffer));
338+
return exchange.getResponse().bufferFactory().wrap(bytes); // just wrapping, no allocation
343339
}
344340
catch (ScriptException ex) {
345341
throw new IllegalStateException("Failed to render script template", new StandardScriptEvalException(ex));
346342
}
347343
catch (Exception ex) {
348344
throw new IllegalStateException("Failed to render script template", ex);
349345
}
350-
});
346+
}));
351347
}
352348

353349
protected String getTemplate(String path) throws IOException {

spring-webflux/src/test/java/org/springframework/web/reactive/resource/PathResourceResolverTests.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -22,6 +22,7 @@
2222
import org.junit.Test;
2323

2424
import org.springframework.core.io.ClassPathResource;
25+
import org.springframework.core.io.FileUrlResource;
2526
import org.springframework.core.io.Resource;
2627
import org.springframework.core.io.UrlResource;
2728

@@ -64,6 +65,22 @@ public void resolveFromClasspathRoot() {
6465
assertNotNull(actual);
6566
}
6667

68+
@Test // gh-22272
69+
public void resolveWithEncodedPath() throws IOException {
70+
Resource classpathLocation = new ClassPathResource("test/", PathResourceResolver.class);
71+
testWithEncodedPath(classpathLocation);
72+
testWithEncodedPath(new FileUrlResource(classpathLocation.getURL()));
73+
}
74+
75+
private void testWithEncodedPath(Resource location) throws IOException {
76+
String path = "foo%20foo.txt";
77+
List<Resource> locations = singletonList(location);
78+
Resource actual = this.resolver.resolveResource(null, path, locations, null).block(TIMEOUT);
79+
80+
assertNotNull(actual);
81+
assertEquals("foo foo.txt", actual.getFile().getName());
82+
}
83+
6784
@Test
6885
public void checkResource() throws IOException {
6986
Resource location = new ClassPathResource("test/", PathResourceResolver.class);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.reactive.result.view;
17+
18+
import java.util.function.Supplier;
19+
20+
import io.netty.buffer.PooledByteBufAllocator;
21+
import org.reactivestreams.Publisher;
22+
import org.reactivestreams.Subscription;
23+
import reactor.core.publisher.BaseSubscriber;
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.core.io.buffer.DataBuffer;
27+
import org.springframework.core.io.buffer.DataBufferFactory;
28+
import org.springframework.core.io.buffer.LeakAwareDataBufferFactory;
29+
import org.springframework.core.io.buffer.NettyDataBufferFactory;
30+
import org.springframework.http.HttpHeaders;
31+
import org.springframework.http.HttpStatus;
32+
import org.springframework.http.ResponseCookie;
33+
import org.springframework.http.server.reactive.ServerHttpResponse;
34+
import org.springframework.util.MultiValueMap;
35+
36+
/**
37+
* Response that subscribes to the writes source but never posts demand and also
38+
* offers method to then cancel the subscription, and check of leaks in the end.
39+
*
40+
* @author Rossen Stoyanchev
41+
*/
42+
public class ZeroDemandResponse implements ServerHttpResponse {
43+
44+
private final LeakAwareDataBufferFactory bufferFactory;
45+
46+
private final ZeroDemandSubscriber writeSubscriber = new ZeroDemandSubscriber();
47+
48+
49+
public ZeroDemandResponse() {
50+
NettyDataBufferFactory delegate = new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT);
51+
this.bufferFactory = new LeakAwareDataBufferFactory(delegate);
52+
}
53+
54+
55+
public void checkForLeaks() {
56+
this.bufferFactory.checkForLeaks();
57+
}
58+
59+
public void cancelWrite() {
60+
this.writeSubscriber.cancel();
61+
}
62+
63+
64+
@Override
65+
public DataBufferFactory bufferFactory() {
66+
return this.bufferFactory;
67+
}
68+
69+
@Override
70+
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
71+
body.subscribe(this.writeSubscriber);
72+
return Mono.never();
73+
}
74+
75+
76+
@Override
77+
public boolean setStatusCode(HttpStatus status) {
78+
throw new UnsupportedOperationException();
79+
}
80+
81+
@Override
82+
public HttpStatus getStatusCode() {
83+
throw new UnsupportedOperationException();
84+
}
85+
86+
@Override
87+
public MultiValueMap<String, ResponseCookie> getCookies() {
88+
throw new UnsupportedOperationException();
89+
}
90+
91+
@Override
92+
public void addCookie(ResponseCookie cookie) {
93+
throw new UnsupportedOperationException();
94+
}
95+
96+
@Override
97+
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
98+
throw new UnsupportedOperationException();
99+
}
100+
101+
@Override
102+
public boolean isCommitted() {
103+
throw new UnsupportedOperationException();
104+
}
105+
106+
@Override
107+
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
108+
throw new UnsupportedOperationException();
109+
}
110+
111+
@Override
112+
public Mono<Void> setComplete() {
113+
throw new UnsupportedOperationException();
114+
}
115+
116+
@Override
117+
public HttpHeaders getHeaders() {
118+
throw new UnsupportedOperationException();
119+
}
120+
121+
122+
private static class ZeroDemandSubscriber extends BaseSubscriber<DataBuffer> {
123+
124+
@Override
125+
protected void hookOnSubscribe(Subscription subscription) {
126+
// Just subscribe without requesting
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)