Move spring-web-reactive classes to spring-web

This commit is contained in:
Rossen Stoyanchev
2016-07-13 23:13:29 -04:00
parent 2e8326220b
commit 5d1b542698
152 changed files with 217 additions and 169 deletions

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import org.springframework.util.Assert;
/**
* Represents an HTTP cookie as a name-value pair consistent with the content of
* the "Cookie" request header. The {@link ResponseCookie} sub-class has the
* additional attributes expected in the "Set-Cookie" response header.
*
* @author Rossen Stoyanchev
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public class HttpCookie {
private final String name;
private final String value;
public HttpCookie(String name, String value) {
Assert.hasLength(name, "'name' is required and must not be empty.");
this.name = name;
this.value = (value != null ? value : "");
}
/**
* Return the cookie name.
*/
public String getName() {
return this.name;
}
/**
* Return the cookie value or an empty string, never {@code null}.
*/
public String getValue() {
return this.value;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof HttpCookie)) {
return false;
}
HttpCookie otherCookie = (HttpCookie) other;
return (this.name.equalsIgnoreCase(otherCookie.getName()));
}
}

View File

@@ -25,6 +25,7 @@ import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
import org.springframework.util.InvalidMimeTypeException;
@@ -445,6 +446,22 @@ public class MediaType extends MimeType implements Serializable {
return result;
}
/**
* Re-create the given mime types as media types.
* @since 5.0
*/
public static List<MediaType> toMediaTypes(List<MimeType> mimeTypes) {
return mimeTypes.stream().map(MediaType::toMediaType).collect(Collectors.toList());
}
/**
* Re-create the given mime type as a media type.
* @since 5.0
*/
public static MediaType toMediaType(MimeType mimeType) {
return new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters());
}
/**
* Return a string representation of the given list of {@code MediaType} objects.
* <p>This method can be used to for an {@code Accept} or {@code Content-Type} header.

View File

@@ -0,0 +1,40 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
/**
* An "reactive" HTTP input message that exposes the input as {@link Publisher}.
*
* <p>Typically implemented by an HTTP request on the server-side or a response
* on the client-side.
*
* @author Arjen Poutsma
*/
public interface ReactiveHttpInputMessage extends HttpMessage {
/**
* Return the body of the message as a {@link Publisher}.
* @return the body content publisher
*/
Flux<DataBuffer> getBody();
}

View File

@@ -0,0 +1,76 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.FlushingDataBuffer;
/**
* A "reactive" HTTP output message that accepts output as a {@link Publisher}.
*
* <p>Typically implemented by an HTTP request on the client-side or a response
* on the server-side.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
*/
public interface ReactiveHttpOutputMessage extends HttpMessage {
/**
* Register an action to be applied just before the message is committed.
* @param action the action
*/
void beforeCommit(Supplier<? extends Mono<Void>> action);
/**
* Use the given {@link Publisher} to write the body of the message to the underlying
* HTTP layer, and flush the data when the complete signal is received (data could be
* flushed before depending on the configuration, the HTTP engine and the amount of
* data sent).
*
* <p>Each {@link FlushingDataBuffer} element will trigger a flush.
*
* @param body the body content publisher
* @return a publisher that indicates completion or error.
*/
Mono<Void> writeWith(Publisher<DataBuffer> body);
/**
* Returns a {@link DataBufferFactory} that can be used for creating the body.
* @return a buffer factory
* @see #writeWith(Publisher)
*/
DataBufferFactory bufferFactory();
/**
* Indicate that message handling is complete, allowing for any cleanup or
* end-of-processing tasks to be performed such as applying header changes
* made via {@link #getHeaders()} to the underlying HTTP message (if not
* applied already).
* <p>This method should be automatically invoked at the end of message
* processing so typically applications should not have to invoke it.
* If invoked multiple times it should have no side effects.
*/
Mono<Void> setComplete();
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.time.Duration;
import java.util.Optional;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* An {@code HttpCookie} sub-class with the additional attributes allowed in
* the "Set-Cookie" response header. To build an instance use the {@link #from}
* static method.
*
* @author Rossen Stoyanchev
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public final class ResponseCookie extends HttpCookie {
private final Duration maxAge;
private final Optional<String> domain;
private final Optional<String> path;
private final boolean secure;
private final boolean httpOnly;
/**
* Private constructor. See {@link #from(String, String)}.
*/
private ResponseCookie(String name, String value, Duration maxAge, String domain,
String path, boolean secure, boolean httpOnly) {
super(name, value);
Assert.notNull(maxAge);
this.maxAge = maxAge;
this.domain = Optional.ofNullable(domain);
this.path = Optional.ofNullable(path);
this.secure = secure;
this.httpOnly = httpOnly;
}
/**
* Return the cookie "Max-Age" attribute in seconds.
*
* <p>A positive value indicates when the cookie expires relative to the
* current time. A value of 0 means the cookie should expire immediately.
* A negative value means no "Max-Age" attribute in which case the cookie
* is removed when the browser is closed.
*/
public Duration getMaxAge() {
return this.maxAge;
}
/**
* Return the cookie "Domain" attribute.
*/
public Optional<String> getDomain() {
return this.domain;
}
/**
* Return the cookie "Path" attribute.
*/
public Optional<String> getPath() {
return this.path;
}
/**
* Return {@code true} if the cookie has the "Secure" attribute.
*/
public boolean isSecure() {
return this.secure;
}
/**
* Return {@code true} if the cookie has the "HttpOnly" attribute.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
public boolean isHttpOnly() {
return this.httpOnly;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain);
result = 31 * result + ObjectUtils.nullSafeHashCode(this.path);
return result;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ResponseCookie)) {
return false;
}
ResponseCookie otherCookie = (ResponseCookie) other;
return (getName().equalsIgnoreCase(otherCookie.getName()) &&
ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) &&
ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain()));
}
/**
* Factory method to obtain a builder for a server-defined cookie that starts
* with a name-value pair and may also include attributes.
* @param name the cookie name
* @param value the cookie value
* @return the created cookie instance
*/
public static ResponseCookieBuilder from(final String name, final String value) {
return new ResponseCookieBuilder() {
private Duration maxAge = Duration.ofSeconds(-1);
private String domain;
private String path;
private boolean secure;
private boolean httpOnly;
@Override
public ResponseCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
return this;
}
@Override
public ResponseCookieBuilder maxAge(long maxAgeSeconds) {
this.maxAge = maxAgeSeconds >= 0 ? Duration.ofSeconds(maxAgeSeconds) : Duration.ofSeconds(-1);
return this;
}
@Override
public ResponseCookieBuilder domain(String domain) {
this.domain = domain;
return this;
}
@Override
public ResponseCookieBuilder path(String path) {
this.path = path;
return this;
}
@Override
public ResponseCookieBuilder secure(boolean secure) {
this.secure = secure;
return this;
}
@Override
public ResponseCookieBuilder httpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
@Override
public ResponseCookie build() {
return new ResponseCookie(name, value, this.maxAge, this.domain, this.path,
this.secure, this.httpOnly);
}
};
}
/**
* A builder for a server-defined HttpCookie with attributes.
*/
public interface ResponseCookieBuilder {
/**
* Set the cookie "Max-Age" attribute.
*
* <p>A positive value indicates when the cookie should expire relative
* to the current time. A value of 0 means the cookie should expire
* immediately. A negative value results in no "Max-Age" attribute in
* which case the cookie is removed when the browser is closed.
*/
ResponseCookieBuilder maxAge(Duration maxAge);
/**
* Set the cookie "Max-Age" attribute in seconds.
*/
ResponseCookieBuilder maxAge(long maxAgeSeconds);
/**
* Set the cookie "Path" attribute.
*/
ResponseCookieBuilder path(String path);
/**
* Set the cookie "Domain" attribute.
*/
ResponseCookieBuilder domain(String domain);
/**
* Add the "Secure" attribute to the cookie.
*/
ResponseCookieBuilder secure(boolean secure);
/**
* Add the "HttpOnly" attribute to the cookie.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
ResponseCookieBuilder httpOnly(boolean httpOnly);
/**
* Create the HttpCookie.
*/
ResponseCookie build();
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.io.File;
import reactor.core.publisher.Mono;
/**
* Sub-interface of {@code ReactiveOutputMessage} that has support for "zero-copy"
* file transfers.
*
* @author Arjen Poutsma
* @see <a href="https://en.wikipedia.org/wiki/Zero-copy">Zero-copy</a>
*/
public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage {
/**
* Use the given {@link File} to write the body of the message to the underlying
* HTTP layer.
* @param file the file to transfer
* @param position the position within the file from which the transfer is to begin
* @param count the number of bytes to be transferred
* @return a publisher that indicates completion or error.
*/
Mono<Void> writeWith(File file, long position, long count);
}

View File

@@ -0,0 +1,107 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Base class for {@link ClientHttpRequest} implementations.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public abstract class AbstractClientHttpRequest implements ClientHttpRequest {
private final HttpHeaders headers;
private final MultiValueMap<String, HttpCookie> cookies;
private AtomicReference<State> state = new AtomicReference<>(State.NEW);
private final List<Supplier<? extends Mono<Void>>> beforeCommitActions = new ArrayList<>(4);
public AbstractClientHttpRequest() {
this(new HttpHeaders());
}
public AbstractClientHttpRequest(HttpHeaders headers) {
Assert.notNull(headers);
this.headers = headers;
this.cookies = new LinkedMultiValueMap<>();
}
@Override
public HttpHeaders getHeaders() {
if (State.COMITTED.equals(this.state.get())) {
return HttpHeaders.readOnlyHttpHeaders(this.headers);
}
return this.headers;
}
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
if (State.COMITTED.equals(this.state.get())) {
return CollectionUtils.unmodifiableMultiValueMap(this.cookies);
}
return this.cookies;
}
protected Mono<Void> applyBeforeCommit() {
Mono<Void> mono = Mono.empty();
if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
for (Supplier<? extends Mono<Void>> action : this.beforeCommitActions) {
mono = mono.then(() -> action.get());
}
return mono
.otherwise(ex -> {
// Ignore errors from beforeCommit actions
return Mono.empty();
})
.then(() -> {
this.state.set(State.COMITTED);
writeHeaders();
writeCookies();
return Mono.empty();
});
}
return mono;
}
@Override
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
Assert.notNull(action);
this.beforeCommitActions.add(action);
}
protected abstract void writeHeaders();
protected abstract void writeCookies();
private enum State {NEW, COMMITTING, COMITTED}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.net.URI;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
/**
* Client abstraction for HTTP client runtimes.
* {@link ClientHttpConnector} drives the underlying HTTP client implementation
* so as to connect to the origin server and provide all the necessary infrastructure
* to send the actual {@link ClientHttpRequest} and receive the {@link ClientHttpResponse}
*
* @author Brian Clozel
*/
public interface ClientHttpConnector {
/**
* Connect to the origin server using the given {@code HttpMethod} and {@code URI},
* then apply the given {@code requestCallback} on the {@link ClientHttpRequest}
* once the connection has been established.
* <p>Return a publisher of the {@link ClientHttpResponse}.
*
* @param method the HTTP request method
* @param uri the HTTP request URI
* @param requestCallback a function that prepares and writes the request,
* returning a publisher that signals when it's done interacting with the request.
* Implementations should return a {@code Mono<Void>} by calling
* {@link ClientHttpRequest#writeWith} or {@link ClientHttpRequest#setComplete}.
* @return a publisher of the {@link ClientHttpResponse}
*/
Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback);
}

View File

@@ -0,0 +1,49 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.net.URI;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpMethod;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.util.MultiValueMap;
/**
* Represents a reactive client-side HTTP request.
*
* @author Arjen Poutsma
* @author Brian Clozel
*/
public interface ClientHttpRequest extends ReactiveHttpOutputMessage {
/**
* Return the HTTP method of the request.
*/
HttpMethod getMethod();
/**
* Return the URI of the request.
*/
URI getURI();
/**
* Return a mutable map of request cookies to send to the server.
*/
MultiValueMap<String, HttpCookie> getCookies();
}

View File

@@ -0,0 +1,41 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ResponseCookie;
import org.springframework.util.MultiValueMap;
/**
* Represents a reactive client-side HTTP response.
*
* @author Arjen Poutsma
*/
public interface ClientHttpResponse extends ReactiveHttpInputMessage {
/**
* @return the HTTP status as an {@link HttpStatus} enum value
*/
HttpStatus getStatusCode();
/**
* Return a read-only map of response cookies received from the server.
*/
MultiValueMap<String, ResponseCookie> getCookies();
}

View File

@@ -0,0 +1,43 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.net.URI;
import java.util.function.Function;
import org.springframework.http.HttpMethod;
import reactor.core.publisher.Mono;
/**
* Reactor-Netty implementation of {@link ClientHttpConnector}
*
* @author Brian Clozel
*/
public class ReactorClientHttpConnector implements ClientHttpConnector {
@Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
return reactor.io.netty.http.HttpClient.create(uri.getHost(), uri.getPort())
.request(io.netty.handler.codec.http.HttpMethod.valueOf(method.name()),
uri.toString(),
httpOutbound -> requestCallback.apply(new ReactorClientHttpRequest(method, uri, httpOutbound)))
.map(httpInbound -> new ReactorClientHttpResponse(httpInbound));
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.net.URI;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.io.netty.http.HttpClient;
import reactor.io.netty.http.HttpClientRequest;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpMethod;
/**
* {@link ClientHttpRequest} implementation for the Reactor-Netty HTTP client
*
* @author Brian Clozel
* @see reactor.io.netty.http.HttpClient
*/
public class ReactorClientHttpRequest extends AbstractClientHttpRequest {
private final HttpMethod httpMethod;
private final URI uri;
private final HttpClientRequest httpRequest;
private final NettyDataBufferFactory bufferFactory;
public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClientRequest httpRequest) {
this.httpMethod = httpMethod;
this.uri = uri;
this.httpRequest = httpRequest;
this.bufferFactory = new NettyDataBufferFactory(httpRequest.delegate().alloc());
}
@Override
public DataBufferFactory bufferFactory() {
return this.bufferFactory;
}
@Override
public HttpMethod getMethod() {
return this.httpMethod;
}
@Override
public URI getURI() {
return this.uri;
}
@Override
public Mono<Void> writeWith(Publisher<DataBuffer> body) {
return applyBeforeCommit()
.then(httpRequest.send(Flux.from(body).map(this::toByteBuf)));
}
@Override
public Mono<Void> setComplete() {
return applyBeforeCommit().then(httpRequest.sendHeaders());
}
private ByteBuf toByteBuf(DataBuffer buffer) {
if (buffer instanceof NettyDataBuffer) {
return ((NettyDataBuffer) buffer).getNativeBuffer();
}
else {
return Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
}
@Override
protected void writeHeaders() {
getHeaders().entrySet().stream()
.forEach(e -> this.httpRequest.headers().set(e.getKey(), e.getValue()));
}
@Override
protected void writeCookies() {
getCookies().values()
.stream().flatMap(cookies -> cookies.stream())
.map(cookie -> new DefaultCookie(cookie.getName(), cookie.getValue()))
.forEach(cookie -> this.httpRequest.addCookie(cookie));
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.util.Collection;
import java.util.function.Function;
import io.netty.buffer.ByteBuf;
import reactor.core.publisher.Flux;
import reactor.io.netty.http.HttpClientResponse;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@link ClientHttpResponse} implementation for the Reactor-Netty HTTP client
*
* @author Brian Clozel
* @see reactor.io.netty.http.HttpClient
*/
public class ReactorClientHttpResponse implements ClientHttpResponse {
private final NettyDataBufferFactory dataBufferFactory;
private final HttpClientResponse response;
public ReactorClientHttpResponse(HttpClientResponse response) {
this.response = response;
this.dataBufferFactory = new NettyDataBufferFactory(response.delegate().alloc());
}
@Override
public Flux<DataBuffer> getBody() {
return response.receive()
.map(buf -> {
buf.retain();
return dataBufferFactory.wrap(buf);
});
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
this.response.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue()));
return headers;
}
@Override
public HttpStatus getStatusCode() {
return HttpStatus.valueOf(this.response.status().code());
}
@Override
public MultiValueMap<String, ResponseCookie> getCookies() {
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
this.response.cookies().values().stream().flatMap(Collection::stream)
.forEach(cookie -> {
ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value())
.domain(cookie.domain())
.path(cookie.path())
.maxAge(cookie.maxAge())
.secure(cookie.isSecure())
.httpOnly(cookie.isHttpOnly())
.build();
result.add(cookie.name(), responseCookie);
});
return CollectionUtils.unmodifiableMultiValueMap(result);
}
@Override
public String toString() {
return "ReactorClientHttpResponse{" +
"request=" + this.response.method().name() + " " + this.response.uri() + "," +
"status=" + getStatusCode() +
'}';
}
}

View File

@@ -0,0 +1,7 @@
/**
* Core package of the reactive client HTTP support.
* Provides {@link org.springframework.http.client.reactive.ClientHttpRequest}
* and {@link org.springframework.http.client.reactive.ClientHttpResponse}
* interfaces and their implementations
*/
package org.springframework.http.client.reactive;

View File

@@ -0,0 +1,164 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec;
import org.springframework.http.MediaType;
import org.springframework.http.codec.SseEventEncoder;
/**
* Represent a Server-Sent Event.
*
* <p>{@code Flux<SseEvent>} is Spring Web Reactive equivalent to Spring MVC
* {@code SseEmitter} type. It allows to send Server-Sent Events in a reactive way.
*
* @author Sebastien Deleuze
* @see SseEventEncoder
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
*/
public class SseEvent {
private String id;
private String name;
private Object data;
private MediaType mediaType;
private Long reconnectTime;
private String comment;
/**
* Create an empty instance.
*/
public SseEvent() {
}
/**
* Create an instance with the provided {@code data}.
*/
public SseEvent(Object data) {
this.data = data;
}
/**
* Create an instance with the provided {@code data} and {@code mediaType}.
*/
public SseEvent(Object data, MediaType mediaType) {
this.data = data;
this.mediaType = mediaType;
}
/**
* Set the {@code id} SSE field
*/
public void setId(String id) {
this.id = id;
}
/**
* @see #setId(String)
*/
public String getId() {
return id;
}
/**
* Set the {@code event} SSE field
*/
public void setName(String name) {
this.name = name;
}
/**
* @see #setName(String)
*/
public String getName() {
return name;
}
/**
* Set {@code data} SSE field. If a multiline {@code String} is provided, it will be
* turned into multiple {@code data} field lines by as
* defined in Server-Sent Events W3C recommendation.
*
* If no {@code mediaType} is defined, default {@link SseEventEncoder} will:
* - Turn single line {@code String} to a single {@code data} field
* - Turn multiline line {@code String} to multiple {@code data} fields
* - Serialize other {@code Object} as JSON
*
* @see #setMediaType(MediaType)
*/
public void setData(Object data) {
this.data = data;
}
/**
* @see #setData(Object)
*/
public Object getData() {
return data;
}
/**
* Set the {@link MediaType} used to serialize the {@code data}.
* {@link SseEventEncoder} should be configured with the relevant encoder to be
* able to serialize it.
*/
public void setMediaType(MediaType mediaType) {
this.mediaType = mediaType;
}
/**
* @see #setMediaType(MediaType)
*/
public MediaType getMediaType() {
return this.mediaType;
}
/**
* Set the {@code retry} SSE field
*/
public void setReconnectTime(Long reconnectTime) {
this.reconnectTime = reconnectTime;
}
/**
* @see #setReconnectTime(Long)
*/
public Long getReconnectTime() {
return reconnectTime;
}
/**
* Set SSE comment. If a multiline comment is provided, it will be turned into multiple
* SSE comment lines by {@link SseEventEncoder} as defined in Server-Sent Events W3C
* recommendation.
*/
public void setComment(String comment) {
this.comment = comment;
}
/**
* @see #setComment(String)
*/
public String getComment() {
return comment;
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.FlushingDataBuffer;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* An encoder for {@link SseEvent}s that also supports any other kind of {@link Object}
* (in that case, the object will be the data of the {@link SseEvent}).
* @author Sebastien Deleuze
*/
public class SseEventEncoder extends AbstractEncoder<Object> {
private final List<Encoder<?>> dataEncoders;
public SseEventEncoder(List<Encoder<?>> dataEncoders) {
super(new MimeType("text", "event-stream"));
Assert.notNull(dataEncoders, "'dataEncoders' must not be null");
this.dataEncoders = dataEncoders;
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType type, MimeType sseMimeType, Object... hints) {
return Flux.from(inputStream).flatMap(input -> {
SseEvent event = (SseEvent.class.equals(type.getRawClass()) ?
(SseEvent)input : new SseEvent(input));
StringBuilder sb = new StringBuilder();
if (event.getId() != null) {
sb.append("id:");
sb.append(event.getId());
sb.append("\n");
}
if (event.getName() != null) {
sb.append("event:");
sb.append(event.getName());
sb.append("\n");
}
if (event.getReconnectTime() != null) {
sb.append("retry:");
sb.append(event.getReconnectTime().toString());
sb.append("\n");
}
if (event.getComment() != null) {
sb.append(":");
sb.append(event.getComment().replaceAll("\\n", "\n:"));
sb.append("\n");
}
Object data = event.getData();
Flux<DataBuffer> dataBuffer = Flux.empty();
MediaType mediaType = (event.getMediaType() == null ?
MediaType.ALL : event.getMediaType());
if (data != null) {
sb.append("data:");
if (data instanceof String) {
sb.append(((String)data).replaceAll("\\n", "\ndata:")).append("\n");
}
else {
dataBuffer = applyEncoder(data, mediaType, bufferFactory);
}
}
// Keep the SSE connection open even for cold stream in order to avoid
// unexpected browser reconnection
return Flux.concat(
encodeString(sb.toString(), bufferFactory),
dataBuffer,
encodeString("\n", bufferFactory),
Mono.just(FlushingDataBuffer.INSTANCE),
Flux.never()
);
});
}
@SuppressWarnings("unchecked")
private <T> Flux<DataBuffer> applyEncoder(Object data, MediaType mediaType, DataBufferFactory bufferFactory) {
ResolvableType elementType = ResolvableType.forClass(data.getClass());
Optional<Encoder<?>> encoder = dataEncoders
.stream()
.filter(e -> e.canEncode(elementType, mediaType))
.findFirst();
if (!encoder.isPresent()) {
return Flux.error(new CodecException("No suitable encoder found!"));
}
return ((Encoder<T>) encoder.get())
.encode(Mono.just((T) data), bufferFactory, elementType, mediaType)
.concatWith(encodeString("\n", bufferFactory));
}
private Mono<DataBuffer> encodeString(String str, DataBufferFactory bufferFactory) {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length).write(bytes);
return Mono.just(buffer);
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.support.DataBufferUtils;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Decode a byte stream into JSON and convert to Object's with Jackson.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*
* @see JacksonJsonEncoder
*/
public class JacksonJsonDecoder extends AbstractDecoder<Object> {
private static final MimeType[] MIME_TYPES = new MimeType[] {
new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8)
};
private final ObjectMapper mapper;
private final JsonObjectDecoder fluxObjectDecoder = new JsonObjectDecoder(true);
private final JsonObjectDecoder monoObjectDecoder = new JsonObjectDecoder(false);
public JacksonJsonDecoder() {
this(new ObjectMapper());
}
public JacksonJsonDecoder(ObjectMapper mapper) {
super(MIME_TYPES);
this.mapper = mapper;
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
JsonObjectDecoder objectDecoder = this.fluxObjectDecoder;
return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints);
}
@Override
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
JsonObjectDecoder objectDecoder = this.monoObjectDecoder;
return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints).single();
}
private Flux<Object> decodeInternal(JsonObjectDecoder objectDecoder, Publisher<DataBuffer> inputStream,
ResolvableType elementType, MimeType mimeType, Object[] hints) {
Assert.notNull(inputStream, "'inputStream' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
TypeFactory typeFactory = this.mapper.getTypeFactory();
JavaType javaType = typeFactory.constructType(elementType.getType());
ObjectReader reader = this.mapper.readerFor(javaType);
return objectDecoder.decode(inputStream, elementType, mimeType, hints)
.map(dataBuffer -> {
try {
Object value = reader.readValue(dataBuffer.asInputStream());
DataBufferUtils.release(dataBuffer);
return value;
}
catch (IOException e) {
return Flux.error(new CodecException("Error while reading the data", e));
}
});
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects.
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @see JacksonJsonDecoder
*/
public class JacksonJsonEncoder extends AbstractEncoder<Object> {
private static final ByteBuffer START_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{'['});
private static final ByteBuffer SEPARATOR_BUFFER = ByteBuffer.wrap(new byte[]{','});
private static final ByteBuffer END_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{']'});
private final ObjectMapper mapper;
public JacksonJsonEncoder() {
this(new ObjectMapper());
}
public JacksonJsonEncoder(ObjectMapper mapper) {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
Assert.notNull(mapper, "'mapper' must not be null");
this.mapper = mapper;
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Object... hints) {
Assert.notNull(inputStream, "'inputStream' must not be null");
Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
if (inputStream instanceof Mono) {
return Flux.from(inputStream).map(value -> encodeValue(value, bufferFactory, elementType));
}
Mono<DataBuffer> startArray = Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER));
Mono<DataBuffer> endArray = Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER));
Flux<DataBuffer> array = Flux.from(inputStream)
.flatMap(value -> {
DataBuffer arraySeparator = bufferFactory.wrap(SEPARATOR_BUFFER);
return Flux.just(encodeValue(value, bufferFactory, elementType), arraySeparator);
});
return Flux.concat(startArray, array.skipLast(1), endArray);
}
private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType type) {
TypeFactory typeFactory = this.mapper.getTypeFactory();
JavaType javaType = typeFactory.constructType(type.getType());
ObjectWriter writer = this.mapper.writerFor(javaType);
DataBuffer buffer = bufferFactory.allocateBuffer();
OutputStream outputStream = buffer.asOutputStream();
try {
writer.writeValue(outputStream, value);
}
catch (IOException e) {
throw new CodecException("Error while writing the data", e);
}
return buffer;
}
}

View File

@@ -0,0 +1,265 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferUtils;
import org.springframework.util.MimeType;
/**
* Decode an arbitrary split byte stream representing JSON objects to a byte
* stream where each chunk is a well-formed JSON object.
*
* This class does not do any real parsing or validation. A sequence of byte
* is considered a JSON object/array if it contains a matching number of opening
* and closing braces/brackets.
*
* Based on <a href="https://github.com/netty/netty/blob/master/codec/src/main/java/io/netty/handler/codec/json/JsonObjectDecoder.java">Netty JsonObjectDecoder</a>
*
* @author Sebastien Deleuze
*/
class JsonObjectDecoder extends AbstractDecoder<DataBuffer> {
private static final int ST_CORRUPTED = -1;
private static final int ST_INIT = 0;
private static final int ST_DECODING_NORMAL = 1;
private static final int ST_DECODING_ARRAY_STREAM = 2;
private final int maxObjectLength;
private final boolean streamArrayElements;
public JsonObjectDecoder() {
// 1 MB
this(1024 * 1024);
}
public JsonObjectDecoder(int maxObjectLength) {
this(maxObjectLength, true);
}
public JsonObjectDecoder(boolean streamArrayElements) {
this(1024 * 1024, streamArrayElements);
}
/**
* @param maxObjectLength maximum number of bytes a JSON object/array may
* use (including braces and all). Objects exceeding this length are dropped
* and an {@link IllegalStateException} is thrown.
* @param streamArrayElements if set to true and the "top level" JSON object
* is an array, each of its entries is passed through the pipeline individually
* and immediately after it was fully received, allowing for arrays with
*/
public JsonObjectDecoder(int maxObjectLength,
boolean streamArrayElements) {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
if (maxObjectLength < 1) {
throw new IllegalArgumentException("maxObjectLength must be a positive int");
}
this.maxObjectLength = maxObjectLength;
this.streamArrayElements = streamArrayElements;
}
@Override
public Flux<DataBuffer> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
return Flux.from(inputStream)
.flatMap(new Function<DataBuffer, Publisher<? extends DataBuffer>>() {
int openBraces;
int index;
int state;
boolean insideString;
ByteBuf input;
Integer writerIndex;
@Override
public Publisher<? extends DataBuffer> apply(DataBuffer b) {
List<DataBuffer> chunks = new ArrayList<>();
if (this.input == null) {
this.input = Unpooled.copiedBuffer(b.asByteBuffer());
DataBufferUtils.release(b);
this.writerIndex = this.input.writerIndex();
}
else {
this.input = Unpooled.copiedBuffer(this.input,
Unpooled.copiedBuffer(b.asByteBuffer()));
DataBufferUtils.release(b);
this.writerIndex = this.input.writerIndex();
}
if (this.state == ST_CORRUPTED) {
this.input.skipBytes(this.input.readableBytes());
return Flux.error(new IllegalStateException("Corrupted stream"));
}
if (this.writerIndex > maxObjectLength) {
// buffer size exceeded maxObjectLength; discarding the complete buffer.
this.input.skipBytes(this.input.readableBytes());
reset();
return Flux.error(new IllegalStateException("object length exceeds " +
maxObjectLength + ": " + this.writerIndex + " bytes discarded"));
}
DataBufferFactory dataBufferFactory = b.factory();
for (/* use current index */; this.index < this.writerIndex; this.index++) {
byte c = this.input.getByte(this.index);
if (this.state == ST_DECODING_NORMAL) {
decodeByte(c, this.input, this.index);
// All opening braces/brackets have been closed. That's enough to conclude
// that the JSON object/array is complete.
if (this.openBraces == 0) {
ByteBuf json = extractObject(this.input, this.input.readerIndex(),
this.index + 1 - this.input.readerIndex());
if (json != null) {
chunks.add(dataBufferFactory.wrap(json.nioBuffer()));
}
// The JSON object/array was extracted => discard the bytes from
// the input buffer.
this.input.readerIndex(this.index + 1);
// Reset the object state to get ready for the next JSON object/text
// coming along the byte stream.
reset();
}
}
else if (this.state == ST_DECODING_ARRAY_STREAM) {
decodeByte(c, this.input, this.index);
if (!this.insideString && (this.openBraces == 1 && c == ',' ||
this.openBraces == 0 && c == ']')) {
// skip leading spaces. No range check is needed and the loop will terminate
// because the byte at position index is not a whitespace.
for (int i = this.input.readerIndex(); Character.isWhitespace(this.input.getByte(i)); i++) {
this.input.skipBytes(1);
}
// skip trailing spaces.
int idxNoSpaces = this.index - 1;
while (idxNoSpaces >= this.input.readerIndex() &&
Character.isWhitespace(this.input.getByte(idxNoSpaces))) {
idxNoSpaces--;
}
ByteBuf json = extractObject(this.input, this.input.readerIndex(),
idxNoSpaces + 1 - this.input.readerIndex());
if (json != null) {
chunks.add(dataBufferFactory.wrap(json.nioBuffer()));
}
this.input.readerIndex(this.index + 1);
if (c == ']') {
reset();
}
}
// JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
}
else if (c == '{' || c == '[') {
initDecoding(c, streamArrayElements);
if (this.state == ST_DECODING_ARRAY_STREAM) {
// Discard the array bracket
this.input.skipBytes(1);
}
// Discard leading spaces in front of a JSON object/array.
}
else if (Character.isWhitespace(c)) {
this.input.skipBytes(1);
}
else {
this.state = ST_CORRUPTED;
return Flux.error(new IllegalStateException(
"invalid JSON received at byte position " + this.index + ": " +
ByteBufUtil.hexDump(this.input)));
}
}
if (this.input.readableBytes() == 0) {
this.index = 0;
}
return Flux.fromIterable(chunks);
}
/**
* Override this method if you want to filter the json objects/arrays that
* get passed through the pipeline.
*/
@SuppressWarnings("UnusedParameters")
protected ByteBuf extractObject(ByteBuf buffer, int index, int length) {
return buffer.slice(index, length).retain();
}
private void decodeByte(byte c, ByteBuf input, int index) {
if ((c == '{' || c == '[') && !this.insideString) {
this.openBraces++;
}
else if ((c == '}' || c == ']') && !this.insideString) {
this.openBraces--;
}
else if (c == '"') {
// start of a new JSON string. It's necessary to detect strings as they may
// also contain braces/brackets and that could lead to incorrect results.
if (!this.insideString) {
this.insideString = true;
// If the double quote wasn't escaped then this is the end of a string.
}
else if (input.getByte(index - 1) != '\\') {
this.insideString = false;
}
}
}
private void initDecoding(byte openingBrace, boolean streamArrayElements) {
this.openBraces = 1;
if (openingBrace == '[' && streamArrayElements) {
this.state = ST_DECODING_ARRAY_STREAM;
}
else {
this.state = ST_DECODING_NORMAL;
}
}
private void reset() {
this.insideString = false;
this.state = ST_INIT;
this.openBraces = 0;
}
});
}
}

View File

@@ -0,0 +1,216 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.xml;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.XmlType;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.XMLEvent;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.codec.CodecException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.xml.StaxUtils;
/**
* Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO).
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @see Jaxb2Encoder
*/
public class Jaxb2Decoder extends AbstractDecoder<Object> {
/**
* The default value for JAXB annotations.
* @see XmlRootElement#name()
* @see XmlRootElement#namespace()
* @see XmlType#name()
* @see XmlType#namespace()
*/
private final static String JAXB_DEFAULT_ANNOTATION_VALUE = "##default";
private final XmlEventDecoder xmlEventDecoder = new XmlEventDecoder();
private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();
public Jaxb2Decoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
}
@Override
public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) {
if (super.canDecode(elementType, mimeType, hints)) {
Class<?> outputClass = elementType.getRawClass();
return outputClass.isAnnotationPresent(XmlRootElement.class) ||
outputClass.isAnnotationPresent(XmlType.class);
}
else {
return false;
}
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
Class<?> outputClass = elementType.getRawClass();
Flux<XMLEvent> xmlEventFlux =
this.xmlEventDecoder.decode(inputStream, null, mimeType);
QName typeName = toQName(outputClass);
Flux<List<XMLEvent>> splitEvents = split(xmlEventFlux, typeName);
return splitEvents.map(events -> unmarshal(events, outputClass));
}
/**
* Returns the qualified name for the given class, according to the mapping rules
* in the JAXB specification.
*/
QName toQName(Class<?> outputClass) {
String localPart;
String namespaceUri;
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
XmlRootElement annotation = outputClass.getAnnotation(XmlRootElement.class);
localPart = annotation.name();
namespaceUri = annotation.namespace();
}
else if (outputClass.isAnnotationPresent(XmlType.class)) {
XmlType annotation = outputClass.getAnnotation(XmlType.class);
localPart = annotation.name();
namespaceUri = annotation.namespace();
}
else {
throw new IllegalArgumentException("Outputclass [" + outputClass + "] is " +
"neither annotated with @XmlRootElement nor @XmlType");
}
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(localPart)) {
localPart = ClassUtils.getShortNameAsProperty(outputClass);
}
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(namespaceUri)) {
Package outputClassPackage = outputClass.getPackage();
if (outputClassPackage != null &&
outputClassPackage.isAnnotationPresent(XmlSchema.class)) {
XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class);
namespaceUri = annotation.namespace();
}
else {
namespaceUri = XMLConstants.NULL_NS_URI;
}
}
return new QName(namespaceUri, localPart);
}
/**
* Split a flux of {@link XMLEvent}s into a flux of XMLEvent lists, one list for each
* branch of the tree that starts with the given qualified name.
* That is, given the XMLEvents shown
* {@linkplain XmlEventDecoder here},
* and the {@code desiredName} "{@code child}", this method
* returns a flux of two lists, each of which containing the events of a particular
* branch of the tree that starts with "{@code child}".
* <ol>
* <li>The first list, dealing with the first branch of the tree
* <ol>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* </ol>
* <li>The second list, dealing with the second branch of the tree
* <ol>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* </ol>
* </li>
* </ol>
*/
Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, QName desiredName) {
return xmlEventFlux
.flatMap(new Function<XMLEvent, Publisher<? extends List<XMLEvent>>>() {
private List<XMLEvent> events = null;
private int elementDepth = 0;
private int barrier = Integer.MAX_VALUE;
@Override
public Publisher<? extends List<XMLEvent>> apply(XMLEvent event) {
if (event.isStartElement()) {
if (this.barrier == Integer.MAX_VALUE) {
QName startElementName = event.asStartElement().getName();
if (desiredName.equals(startElementName)) {
this.events = new ArrayList<XMLEvent>();
this.barrier = this.elementDepth;
}
}
this.elementDepth++;
}
if (this.elementDepth > this.barrier) {
this.events.add(event);
}
if (event.isEndElement()) {
this.elementDepth--;
if (this.elementDepth == this.barrier) {
this.barrier = Integer.MAX_VALUE;
return Mono.just(this.events);
}
}
return Mono.empty();
}
});
}
private Object unmarshal(List<XMLEvent> events, Class<?> outputClass) {
try {
Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass);
XMLEventReader eventReader = StaxUtils.createXMLEventReader(events);
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
return unmarshaller.unmarshal(eventReader);
}
else {
JAXBElement<?> jaxbElement =
unmarshaller.unmarshal(eventReader, outputClass);
return jaxbElement.getValue();
}
}
catch (JAXBException ex) {
throw new CodecException(ex.getMessage(), ex);
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.xml;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractSingleValueEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Encode from an {@code Object} stream to a byte stream of XML elements.
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @see Jaxb2Decoder
*/
public class Jaxb2Encoder extends AbstractSingleValueEncoder<Object> {
private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();
public Jaxb2Encoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) {
if (super.canEncode(elementType, mimeType, hints)) {
Class<?> outputClass = elementType.getRawClass();
return outputClass.isAnnotationPresent(XmlRootElement.class) ||
outputClass.isAnnotationPresent(XmlType.class);
}
else {
return false;
}
}
@Override
protected Flux<DataBuffer> encode(Object value, DataBufferFactory dataBufferFactory,
ResolvableType type, MimeType mimeType, Object... hints) {
try {
DataBuffer buffer = dataBufferFactory.allocateBuffer(1024);
OutputStream outputStream = buffer.asOutputStream();
Class<?> clazz = ClassUtils.getUserClass(value);
Marshaller marshaller = jaxbContexts.createMarshaller(clazz);
marshaller
.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
marshaller.marshal(value, outputStream);
return Flux.just(buffer);
}
catch (JAXBException ex) {
return Flux.error(ex);
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.xml;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.springframework.util.Assert;
/**
* @author Arjen Poutsma
*/
final class JaxbContextContainer {
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts =
new ConcurrentHashMap<>(64);
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException {
JAXBContext jaxbContext = getJaxbContext(clazz);
return jaxbContext.createMarshaller();
}
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
JAXBContext jaxbContext = getJaxbContext(clazz);
return jaxbContext.createUnmarshaller();
}
private JAXBContext getJaxbContext(Class<?> clazz) throws JAXBException {
Assert.notNull(clazz, "'clazz' must not be null");
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
if (jaxbContext == null) {
jaxbContext = JAXBContext.newInstance(clazz);
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
}
return jaxbContext;
}
}

View File

@@ -0,0 +1,156 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.xml;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.util.XMLEventAllocator;
import com.fasterxml.aalto.AsyncByteBufferFeeder;
import com.fasterxml.aalto.AsyncXMLInputFactory;
import com.fasterxml.aalto.AsyncXMLStreamReader;
import com.fasterxml.aalto.evt.EventAllocatorImpl;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.support.DataBufferUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Decodes a {@link DataBuffer} stream into a stream of {@link XMLEvent}s. That is, given
* the following XML:
* <pre>{@code
* <root>
* <child>foo</child>
* <child>bar</child>
* </root>}
* </pre>
* this method with result in a flux with the following events:
* <ol>
* <li>{@link javax.xml.stream.events.StartDocument}</li>
* <li>{@link javax.xml.stream.events.StartElement} {@code root}</li>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code root}</li>
* </ol>
*
* Note that this decoder is not registered by default, but used internally by other
* decoders who are.
*
* @author Arjen Poutsma
*/
public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
private static final boolean aaltoPresent = ClassUtils
.isPresent("com.fasterxml.aalto.AsyncXMLStreamReader",
XmlEventDecoder.class.getClassLoader());
private static final XMLInputFactory inputFactory = XMLInputFactory.newFactory();
boolean useAalto = true;
public XmlEventDecoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
}
@Override
@SuppressWarnings("unchecked")
public Flux<XMLEvent> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
Flux<DataBuffer> flux = Flux.from(inputStream);
if (useAalto && aaltoPresent) {
return flux.flatMap(new AaltoDataBufferToXmlEvent());
}
else {
Mono<DataBuffer> singleBuffer = flux.reduce(DataBuffer::write);
return singleBuffer.
flatMap(dataBuffer -> {
try {
InputStream is = dataBuffer.asInputStream();
XMLEventReader eventReader = inputFactory.createXMLEventReader(is);
return Flux.fromIterable((Iterable<XMLEvent>) () -> eventReader);
}
catch (XMLStreamException ex) {
return Mono.error(ex);
}
finally {
DataBufferUtils.release(dataBuffer);
}
});
}
}
/*
* Separate static class to isolate Aalto dependency.
*/
private static class AaltoDataBufferToXmlEvent
implements Function<DataBuffer, Publisher<? extends XMLEvent>> {
private static final AsyncXMLInputFactory inputFactory =
(AsyncXMLInputFactory) XmlEventDecoder.inputFactory;
private final AsyncXMLStreamReader<AsyncByteBufferFeeder> streamReader =
inputFactory.createAsyncForByteBuffer();
private final XMLEventAllocator eventAllocator =
EventAllocatorImpl.getDefaultInstance();
@Override
public Publisher<? extends XMLEvent> apply(DataBuffer dataBuffer) {
try {
streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer());
List<XMLEvent> events = new ArrayList<>();
while (true) {
if (streamReader.next() == AsyncXMLStreamReader.EVENT_INCOMPLETE) {
// no more events with what currently has been fed to the reader
break;
}
else {
XMLEvent event = eventAllocator.allocate(streamReader);
events.add(event);
if (event.isEndDocument()) {
break;
}
}
}
return Flux.fromIterable(events);
}
catch (XMLStreamException ex) {
return Mono.error(ex);
}
finally {
DataBufferUtils.release(dataBuffer);
}
}
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.reactive;
import java.util.Collections;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ReactiveHttpOutputMessage;
/**
* Implementation of the {@link HttpMessageConverter} interface that delegates to
* {@link Encoder} and {@link Decoder}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/
public class CodecHttpMessageConverter<T> implements HttpMessageConverter<T> {
private final Encoder<T> encoder;
private final Decoder<T> decoder;
private final List<MediaType> readableMediaTypes;
private final List<MediaType> writableMediaTypes;
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Encoder}. When
* using this constructor, all read-related methods will in {@code false} or an
* {@link IllegalStateException}.
* @param encoder the encoder to use
*/
public CodecHttpMessageConverter(Encoder<T> encoder) {
this(encoder, null);
}
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Decoder}. When
* using this constructor, all write-related methods will in {@code false} or an
* {@link IllegalStateException}.
* @param decoder the decoder to use
*/
public CodecHttpMessageConverter(Decoder<T> decoder) {
this(null, decoder);
}
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Encoder} and
* {@link Decoder}.
* @param encoder the encoder to use, can be {@code null}
* @param decoder the decoder to use, can be {@code null}
*/
public CodecHttpMessageConverter(Encoder<T> encoder, Decoder<T> decoder) {
this.encoder = encoder;
this.decoder = decoder;
this.readableMediaTypes = decoder != null ?
MediaType.toMediaTypes(decoder.getDecodableMimeTypes()) :
Collections.emptyList();
this.writableMediaTypes = encoder != null ?
MediaType.toMediaTypes(encoder.getEncodableMimeTypes()) :
Collections.emptyList();
}
@Override
public boolean canRead(ResolvableType type, MediaType mediaType) {
return this.decoder != null && this.decoder.canDecode(type, mediaType);
}
@Override
public boolean canWrite(ResolvableType type, MediaType mediaType) {
return this.encoder != null && this.encoder.canEncode(type, mediaType);
}
@Override
public List<MediaType> getReadableMediaTypes() {
return this.readableMediaTypes;
}
@Override
public List<MediaType> getWritableMediaTypes() {
return this.writableMediaTypes;
}
@Override
public Flux<T> read(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Flux.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decode(inputMessage.getBody(), type, contentType);
}
@Override
public Mono<T> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Mono.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decodeToMono(inputMessage.getBody(), type, contentType);
}
private MediaType getContentType(ReactiveHttpInputMessage inputMessage) {
MediaType contentType = inputMessage.getHeaders().getContentType();
return (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {
if (this.encoder == null) {
return Mono.error(new IllegalStateException("No decoder set"));
}
HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(type);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(type);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
MediaType mediaType = getDefaultContentType(type);
if (mediaType != null && mediaType.getCharset() != null) {
contentTypeToUse = new MediaType(contentTypeToUse, mediaType.getCharset());
}
}
headers.setContentType(contentTypeToUse);
}
}
DataBufferFactory bufferFactory = outputMessage.bufferFactory();
Flux<DataBuffer> body = this.encoder.encode(inputStream, bufferFactory, type, contentType);
return outputMessage.writeWith(body);
}
/**
* Return the default content type for the given {@code ResolvableType}.
* Used when {@link #write} is called without a concrete content type.
*
* <p>By default returns the first of {@link Encoder#getEncodableMimeTypes()
* encodableMimeTypes}, if any.
*
* @param elementType the type of element for encoding
* @return the content type, or {@code null}
*/
@SuppressWarnings("UnusedParameters")
protected MediaType getDefaultContentType(ResolvableType elementType) {
return (!this.writableMediaTypes.isEmpty() ? this.writableMediaTypes.get(0) : null);
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.reactive;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ReactiveHttpOutputMessage;
/**
* Strategy interface that specifies a converter that can convert from and to HTTP
* requests and responses.
* @author Arjen Poutsma
*/
public interface HttpMessageConverter<T> {
/**
* Indicates whether the given class can be read by this converter.
* @param type the type to test for readability
* @param mediaType the media type to read, can be {@code null} if not specified.
* Typically the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise
*/
boolean canRead(ResolvableType type, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects that can be read by this converter.
* @return the list of supported readable media types
*/
List<MediaType> getReadableMediaTypes();
/**
* Read a {@link Flux} of the given type form the given input message, and returns it.
* @param type the type of object to return. This type must have previously been
* passed to the
* {@link #canRead canRead} method of this interface, which must have returned {@code
* true}.
* @param inputMessage the HTTP input message to read from
* @return the converted {@link Flux} of elements
*/
Flux<T> read(ResolvableType type, ReactiveHttpInputMessage inputMessage);
/**
* Read a {@link Mono} of the given type form the given input message, and returns it.
* @param type the type of object to return. This type must have previously been
* passed to the
* {@link #canRead canRead} method of this interface, which must have returned {@code
* true}.
* @param inputMessage the HTTP input message to read from
* @return the converted {@link Mono} of object
*/
Mono<T> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage);
/**
* Indicates whether the given class can be written by this converter.
* @param type the class to test for writability
* @param mediaType the media type to write, can be {@code null} if not specified.
* Typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(ResolvableType type, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects that can be written by this
* converter.
* @return the list of supported readable media types
*/
List<MediaType> getWritableMediaTypes();
/**
* Write an given object to the given output message.
* @param inputStream the input stream to write
* @param type the stream element type to process.
* @param contentType the content type to use when writing. May be {@code null} to
* indicate that the default content type of the converter must be used.
* @param outputMessage the message to write to
* @return
*/
Mono<Void> write(Publisher<? extends T> inputStream,
ResolvableType type, MediaType contentType,
ReactiveHttpOutputMessage outputMessage);
}

View File

@@ -0,0 +1,130 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.reactive;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.ResourceEncoder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ResourceUtils;
/**
* Implementation of {@link HttpMessageConverter} that can read and write
* {@link Resource Resources} and supports byte range requests.
**
* @author Arjen Poutsma
*/
public class ResourceHttpMessageConverter extends CodecHttpMessageConverter<Resource> {
public ResourceHttpMessageConverter() {
super(new ResourceEncoder(), new ResourceDecoder());
}
public ResourceHttpMessageConverter(int bufferSize) {
super(new ResourceEncoder(bufferSize), new ResourceDecoder());
}
@Override
public Mono<Void> write(Publisher<? extends Resource> inputStream,
ResolvableType type, MediaType contentType,
ReactiveHttpOutputMessage outputMessage) {
return Mono.from(Flux.from(inputStream).
take(1).
concatMap(resource -> {
HttpHeaders headers = outputMessage.getHeaders();
addHeaders(headers, resource, contentType);
return writeContent(resource, type, contentType, outputMessage);
}));
}
protected void addHeaders(HttpHeaders headers, Resource resource,
MediaType contentType) {
if (headers.getContentType() == null) {
if (contentType == null ||
!contentType.isConcrete() ||
MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
contentType = MimeTypeUtils.getMimeType(resource.getFilename()).
map(MediaType::toMediaType).
orElse(MediaType.APPLICATION_OCTET_STREAM);
}
headers.setContentType(contentType);
}
if (headers.getContentLength() < 0) {
contentLength(resource).ifPresent(headers::setContentLength);
}
}
private Mono<Void> writeContent(Resource resource, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {
if (outputMessage instanceof ZeroCopyHttpOutputMessage) {
Optional<File> file = getFile(resource);
if (file.isPresent()) {
ZeroCopyHttpOutputMessage zeroCopyResponse =
(ZeroCopyHttpOutputMessage) outputMessage;
return zeroCopyResponse
.writeWith(file.get(), (long) 0, file.get().length());
}
}
// non-zero copy fallback, using ResourceEncoder
return super.write(Mono.just(resource), type,
outputMessage.getHeaders().getContentType(), outputMessage);
}
private static Optional<Long> contentLength(Resource resource) {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
if (InputStreamResource.class != resource.getClass()) {
try {
return Optional.of(resource.contentLength());
}
catch (IOException ignored) {
}
}
return Optional.empty();
}
private static Optional<File> getFile(Resource resource) {
if (ResourceUtils.hasFile(resource)) {
try {
return Optional.of(resource.getFile());
}
catch (IOException ignored) {
// should not happen
}
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,316 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.nio.channels.Channel;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ReadListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.util.BackpressureUtils;
import org.springframework.core.io.buffer.DataBuffer;
/**
* Abstract base class for {@code Publisher} implementations that bridge between
* event-listener APIs and Reactive Streams. Specifically, base class for the Servlet 3.1
* and Undertow support.
*
* @author Arjen Poutsma
* @see ServletServerHttpRequest
* @see UndertowHttpHandlerAdapter
*/
abstract class AbstractRequestBodyPublisher implements Publisher<DataBuffer> {
protected final Log logger = LogFactory.getLog(getClass());
private final AtomicReference<State> state =
new AtomicReference<>(State.UNSUBSCRIBED);
private final AtomicLong demand = new AtomicLong();
private Subscriber<? super DataBuffer> subscriber;
@Override
public void subscribe(Subscriber<? super DataBuffer> subscriber) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.state + " subscribe: " + subscriber);
}
this.state.get().subscribe(this, subscriber);
}
/**
* Called via a listener interface to indicate that reading is possible.
* @see ReadListener#onDataAvailable()
* @see org.xnio.ChannelListener#handleEvent(Channel)
*/
protected final void onDataAvailable() {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.state + " onDataAvailable");
}
this.state.get().onDataAvailable(this);
}
/**
* Called via a listener interface to indicate that all data has been read.
* @see ReadListener#onAllDataRead()
* @see org.xnio.ChannelListener#handleEvent(Channel)
*/
protected final void onAllDataRead() {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.state + " onAllDataRead");
}
this.state.get().onAllDataRead(this);
}
/**
* Called by a listener interface to indicate that as error has occured.
* @param t the error
* @see ReadListener#onError(Throwable)
*/
protected final void onError(Throwable t) {
if (this.logger.isErrorEnabled()) {
this.logger.error(this.state + " onError: " + t, t);
}
this.state.get().onError(this, t);
}
/**
* Reads and publishes data buffers from the input. Continues till either there is no
* more demand, or till there is no more data to be read.
* @return {@code true} if there is more demand; {@code false} otherwise
*/
private boolean readAndPublish() throws IOException {
while (hasDemand()) {
DataBuffer dataBuffer = read();
if (dataBuffer != null) {
BackpressureUtils.getAndSub(this.demand, 1L);
this.subscriber.onNext(dataBuffer);
}
else {
return true;
}
}
return false;
}
protected abstract void checkOnDataAvailable();
/**
* Reads a data buffer from the input, if possible. Returns {@code null} if a buffer
* could not be read.
* @return the data buffer that was read; or {@code null}
*/
protected abstract DataBuffer read() throws IOException;
private boolean hasDemand() {
return this.demand.get() > 0;
}
private boolean changeState(AbstractRequestBodyPublisher.State oldState,
AbstractRequestBodyPublisher.State newState) {
return this.state.compareAndSet(oldState, newState);
}
private static final class RequestBodySubscription implements Subscription {
private final AbstractRequestBodyPublisher publisher;
public RequestBodySubscription(AbstractRequestBodyPublisher publisher) {
this.publisher = publisher;
}
@Override
public final void request(long n) {
if (this.publisher.logger.isTraceEnabled()) {
this.publisher.logger.trace(state() + " request: " + n);
}
state().request(this.publisher, n);
}
@Override
public final void cancel() {
if (this.publisher.logger.isTraceEnabled()) {
this.publisher.logger.trace(state() + " cancel");
}
state().cancel(this.publisher);
}
private AbstractRequestBodyPublisher.State state() {
return this.publisher.state.get();
}
}
/**
* Represents a state for the {@link Publisher} to be in. The following figure
* indicate the four different states that exist, and the relationships between them.
*
* <pre>
* UNSUBSCRIBED
* |
* v
* NO_DEMAND -------------------> DEMAND
* | ^ ^ |
* | | | |
* | --------- READING <----- |
* | | |
* | v |
* ------------> COMPLETED <---------
* </pre>
* Refer to the individual states for more information.
*/
private enum State {
/**
* The initial unsubscribed state. Will respond to {@link
* #subscribe(AbstractRequestBodyPublisher, Subscriber)} by
* changing state to {@link #NO_DEMAND}.
*/
UNSUBSCRIBED {
@Override
void subscribe(AbstractRequestBodyPublisher publisher,
Subscriber<? super DataBuffer> subscriber) {
Objects.requireNonNull(subscriber);
if (publisher.changeState(this, NO_DEMAND)) {
Subscription subscription = new RequestBodySubscription(
publisher);
publisher.subscriber = subscriber;
subscriber.onSubscribe(subscription);
}
else {
throw new IllegalStateException(toString());
}
}
},
/**
* State that gets entered when there is no demand. Responds to {@link
* #request(AbstractRequestBodyPublisher, long)} by increasing the demand,
* changing state to {@link #DEMAND} and will check whether there
* is data available for reading.
*/
NO_DEMAND {
@Override
void request(AbstractRequestBodyPublisher publisher, long n) {
if (BackpressureUtils.checkRequest(n, publisher.subscriber)) {
BackpressureUtils.addAndGet(publisher.demand, n);
if (publisher.changeState(this, DEMAND)) {
publisher.checkOnDataAvailable();
}
}
}
},
/**
* State that gets entered when there is demand. Responds to
* {@link #onDataAvailable(AbstractRequestBodyPublisher)} by
* reading the available data. The state will be changed to
* {@link #NO_DEMAND} if there is no demand.
*/
DEMAND {
@Override
void onDataAvailable(AbstractRequestBodyPublisher publisher) {
if (publisher.changeState(this, READING)) {
try {
boolean demandAvailable = publisher.readAndPublish();
if (demandAvailable) {
publisher.changeState(READING, DEMAND);
publisher.checkOnDataAvailable();
} else {
publisher.changeState(READING, NO_DEMAND);
}
} catch (IOException ex) {
publisher.onError(ex);
}
}
}
},
READING {
@Override
void request(AbstractRequestBodyPublisher publisher, long n) {
if (BackpressureUtils.checkRequest(n, publisher.subscriber)) {
BackpressureUtils.addAndGet(publisher.demand, n);
}
}
},
/**
* The terminal completed state. Does not respond to any events.
*/
COMPLETED {
@Override
void request(AbstractRequestBodyPublisher publisher, long n) {
// ignore
}
@Override
void cancel(AbstractRequestBodyPublisher publisher) {
// ignore
}
@Override
void onAllDataRead(AbstractRequestBodyPublisher publisher) {
// ignore
}
@Override
void onError(AbstractRequestBodyPublisher publisher, Throwable t) {
// ignore
}
};
void subscribe(AbstractRequestBodyPublisher publisher,
Subscriber<? super DataBuffer> subscriber) {
throw new IllegalStateException(toString());
}
void request(AbstractRequestBodyPublisher publisher, long n) {
throw new IllegalStateException(toString());
}
void cancel(AbstractRequestBodyPublisher publisher) {
publisher.changeState(this, COMPLETED);
}
void onDataAvailable(AbstractRequestBodyPublisher publisher) {
// ignore
}
void onAllDataRead(AbstractRequestBodyPublisher publisher) {
if (publisher.changeState(this, COMPLETED)) {
if (publisher.subscriber != null) {
publisher.subscriber.onComplete();
}
}
}
void onError(AbstractRequestBodyPublisher publisher, Throwable t) {
if (publisher.changeState(this, COMPLETED)) {
if (publisher.subscriber != null) {
publisher.subscriber.onError(t);
}
}
}
}
}

View File

@@ -0,0 +1,329 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.nio.channels.Channel;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.WriteListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Processor;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.FlushingDataBuffer;
import org.springframework.core.io.buffer.support.DataBufferUtils;
import org.springframework.util.Assert;
/**
* Abstract base class for {@code Subscriber} implementations that bridge between
* event-listener APIs and Reactive Streams. Specifically, base class for the Servlet 3.1
* and Undertow support.
* @author Arjen Poutsma
* @see ServletServerHttpRequest
* @see UndertowHttpHandlerAdapter
*/
abstract class AbstractResponseBodyProcessor implements Processor<DataBuffer, Void> {
protected final Log logger = LogFactory.getLog(getClass());
private final ResponseBodyWriteResultPublisher publisherDelegate =
new ResponseBodyWriteResultPublisher();
private final AtomicReference<State> state =
new AtomicReference<>(State.UNSUBSCRIBED);
private volatile DataBuffer currentBuffer;
private volatile boolean subscriberCompleted;
private Subscription subscription;
// Subscriber
@Override
public final void onSubscribe(Subscription subscription) {
if (logger.isTraceEnabled()) {
logger.trace(this.state + " onSubscribe: " + subscription);
}
this.state.get().onSubscribe(this, subscription);
}
@Override
public final void onNext(DataBuffer dataBuffer) {
if (logger.isTraceEnabled()) {
logger.trace(this.state + " onNext: " + dataBuffer);
}
this.state.get().onNext(this, dataBuffer);
}
@Override
public final void onError(Throwable t) {
if (logger.isErrorEnabled()) {
logger.error(this.state + " onError: " + t, t);
}
this.state.get().onError(this, t);
}
@Override
public final void onComplete() {
if (logger.isTraceEnabled()) {
logger.trace(this.state + " onComplete");
}
this.state.get().onComplete(this);
}
// Publisher
@Override
public final void subscribe(Subscriber<? super Void> subscriber) {
this.publisherDelegate.subscribe(subscriber);
}
// listener methods
/**
* Called via a listener interface to indicate that writing is possible.
* @see WriteListener#onWritePossible()
* @see org.xnio.ChannelListener#handleEvent(Channel)
*/
protected final void onWritePossible() {
this.state.get().onWritePossible(this);
}
/**
* Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)}
* @param dataBuffer the buffer that was received.
*/
protected void receiveBuffer(DataBuffer dataBuffer) {
Assert.state(this.currentBuffer == null);
this.currentBuffer = dataBuffer;
}
/**
* Called when the current buffer should be
* {@linkplain DataBufferUtils#release(DataBuffer) released}.
*/
protected void releaseBuffer() {
if (logger.isTraceEnabled()) {
logger.trace("releaseBuffer: " + this.currentBuffer);
}
DataBufferUtils.release(this.currentBuffer);
this.currentBuffer = null;
}
/**
* Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)}
* or when only partial data from the {@link DataBuffer} was written.
*/
private void writeIfPossible() {
if (isWritePossible()) {
onWritePossible();
}
}
/**
* Called via a listener interface to determine whether writing is possible.
*/
protected boolean isWritePossible() {
return false;
}
/**
* Writes the given data buffer to the output, indicating if the entire buffer was
* written.
* @param dataBuffer the data buffer to write
* @return {@code true} if {@code dataBuffer} was fully written and a new buffer
* can be requested; {@code false} otherwise
*/
protected abstract boolean write(DataBuffer dataBuffer) throws IOException;
/**
* Flushes the output.
*/
protected abstract void flush() throws IOException;
private boolean changeState(State oldState, State newState) {
return this.state.compareAndSet(oldState, newState);
}
/**
* Represents a state for the {@link Subscriber} to be in. The following figure
* indicate the four different states that exist, and the relationships between them.
*
* <pre>
* UNSUBSCRIBED
* |
* v
* REQUESTED -------------------> RECEIVED
* ^ ^
* | |
* --------- WRITING <-----
* |
* v
* COMPLETED
* </pre>
* Refer to the individual states for more information.
*/
private enum State {
/**
* The initial unsubscribed state. Will respond to {@code onSubscribe} by
* requesting 1 buffer from the subscription, and change state to {@link
* #REQUESTED}.
*/
UNSUBSCRIBED {
@Override
void onSubscribe(AbstractResponseBodyProcessor processor,
Subscription subscription) {
Objects.requireNonNull(subscription, "Subscription cannot be null");
if (processor.changeState(this, REQUESTED)) {
processor.subscription = subscription;
subscription.request(1);
}
else {
super.onSubscribe(processor, subscription);
}
}
},
/**
* State that gets entered after a buffer has been
* {@linkplain Subscription#request(long) requested}. Responds to {@code onNext}
* by changing state to {@link #RECEIVED}, and responds to {@code onComplete} by
* changing state to {@link #COMPLETED}.
*/
REQUESTED {
@Override
void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) {
if (processor.changeState(this, RECEIVED)) {
processor.receiveBuffer(dataBuffer);
processor.writeIfPossible();
}
}
@Override
void onComplete(AbstractResponseBodyProcessor processor) {
if (processor.changeState(this, COMPLETED)) {
processor.subscriberCompleted = true;
processor.publisherDelegate.publishComplete();
}
}
},
/**
* State that gets entered after a buffer has been
* {@linkplain Subscriber#onNext(Object) received}. Responds to
* {@code onWritePossible} by writing the current buffer and changes
* the state to {@link #WRITING}. If it can be written completely,
* changes the state to either {@link #REQUESTED} if the subscription
* has not been completed; or {@link #COMPLETED} if it has. If it cannot
* be written completely the state will be changed to {@link #RECEIVED}.
*/
RECEIVED {
@Override
void onWritePossible(AbstractResponseBodyProcessor processor) {
if (processor.changeState(this, WRITING)) {
DataBuffer dataBuffer = processor.currentBuffer;
try {
boolean writeCompleted = processor.write(dataBuffer);
if (writeCompleted) {
if (dataBuffer instanceof FlushingDataBuffer) {
processor.flush();
}
processor.releaseBuffer();
if (!processor.subscriberCompleted) {
processor.changeState(WRITING, REQUESTED);
processor.subscription.request(1);
}
else {
processor.changeState(WRITING, COMPLETED);
processor.publisherDelegate.publishComplete();
}
}
else {
processor.changeState(WRITING, RECEIVED);
processor.writeIfPossible();
}
}
catch (IOException ex) {
processor.onError(ex);
}
}
}
@Override
void onComplete(AbstractResponseBodyProcessor processor) {
processor.subscriberCompleted = true;
}
},
/**
* State that gets entered after a writing of the current buffer has been
* {@code onWritePossible started}.
*/
WRITING {
@Override
void onComplete(AbstractResponseBodyProcessor processor) {
processor.subscriberCompleted = true;
}
},
/**
* The terminal completed state. Does not respond to any events.
*/
COMPLETED {
@Override
void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) {
// ignore
}
@Override
void onError(AbstractResponseBodyProcessor processor, Throwable t) {
// ignore
}
@Override
void onComplete(AbstractResponseBodyProcessor processor) {
// ignore
}
};
void onSubscribe(AbstractResponseBodyProcessor processor, Subscription s) {
s.cancel();
}
void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) {
throw new IllegalStateException(toString());
}
void onError(AbstractResponseBodyProcessor processor, Throwable t) {
if (processor.changeState(this, COMPLETED)) {
processor.publisherDelegate.publishError(t);
}
}
void onComplete(AbstractResponseBodyProcessor processor) {
throw new IllegalStateException(toString());
}
void onWritePossible(AbstractResponseBodyProcessor processor) {
// ignore
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Common base class for {@link ServerHttpRequest} implementations.
*
* @author Rossen Stoyanchev
*/
public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");
private URI uri;
private MultiValueMap<String, String> queryParams;
private HttpHeaders headers;
private MultiValueMap<String, HttpCookie> cookies;
@Override
public URI getURI() {
if (this.uri == null) {
try {
this.uri = initUri();
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex);
}
}
return this.uri;
}
/**
* Initialize a URI that represents the request. Invoked lazily on the first
* call to {@link #getURI()} and then cached.
* @throws URISyntaxException
*/
protected abstract URI initUri() throws URISyntaxException;
@Override
public MultiValueMap<String, String> getQueryParams() {
if (this.queryParams == null) {
this.queryParams = CollectionUtils.unmodifiableMultiValueMap(initQueryParams());
}
return this.queryParams;
}
protected MultiValueMap<String, String> initQueryParams() {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
String query = getURI().getRawQuery();
if (query != null) {
Matcher matcher = QUERY_PATTERN.matcher(query);
while (matcher.find()) {
String name = matcher.group(1);
String eq = matcher.group(2);
String value = matcher.group(3);
value = (value != null ? value : (StringUtils.hasLength(eq) ? "" : null));
queryParams.add(name, value);
}
}
return queryParams;
}
@Override
public HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = HttpHeaders.readOnlyHttpHeaders(initHeaders());
}
return this.headers;
}
/**
* Initialize the headers from the underlying request. Invoked lazily on the
* first call to {@link #getHeaders()} and then cached.
*/
protected abstract HttpHeaders initHeaders();
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
if (this.cookies == null) {
this.cookies = CollectionUtils.unmodifiableMultiValueMap(initCookies());
}
return this.cookies;
}
/**
* Initialize the cookies from the underlying request. Invoked lazily on the
* first access to cookies via {@link #getHeaders()} and then cached.
*/
protected abstract MultiValueMap<String, HttpCookie> initCookies();
}

View File

@@ -0,0 +1,186 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Base class for {@link ServerHttpResponse} implementations.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
private Log logger = LogFactory.getLog(getClass());
private static final int STATE_NEW = 1;
private static final int STATE_COMMITTING = 2;
private static final int STATE_COMMITTED = 3;
private final DataBufferFactory dataBufferFactory;
private HttpStatus statusCode;
private final HttpHeaders headers;
private final MultiValueMap<String, ResponseCookie> cookies;
private final List<Supplier<? extends Mono<Void>>> beforeCommitActions = new ArrayList<>(4);
private final AtomicInteger state = new AtomicInteger(STATE_NEW);
public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory) {
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.dataBufferFactory = dataBufferFactory;
this.headers = new HttpHeaders();
this.cookies = new LinkedMultiValueMap<String, ResponseCookie>();
}
@Override
public final DataBufferFactory bufferFactory() {
return this.dataBufferFactory;
}
@Override
public boolean setStatusCode(HttpStatus statusCode) {
Assert.notNull(statusCode);
if (STATE_NEW == this.state.get()) {
this.statusCode = statusCode;
return true;
}
else if (logger.isDebugEnabled()) {
logger.debug("Can't set the status " + statusCode.toString() +
" because the HTTP response has already been committed");
}
return false;
}
@Override
public HttpStatus getStatusCode() {
return this.statusCode;
}
@Override
public HttpHeaders getHeaders() {
if (STATE_COMMITTED == this.state.get()) {
return HttpHeaders.readOnlyHttpHeaders(this.headers);
}
else {
return this.headers;
}
}
@Override
public MultiValueMap<String, ResponseCookie> getCookies() {
if (STATE_COMMITTED == this.state.get()) {
return CollectionUtils.unmodifiableMultiValueMap(this.cookies);
}
return this.cookies;
}
@Override
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
Assert.notNull(action);
this.beforeCommitActions.add(action);
}
@Override
public Mono<Void> writeWith(Publisher<DataBuffer> publisher) {
return new ChannelSendOperator<>(publisher, writePublisher ->
applyBeforeCommit().then(() -> writeWithInternal(writePublisher)));
}
protected Mono<Void> applyBeforeCommit() {
Mono<Void> mono = Mono.empty();
if (this.state.compareAndSet(STATE_NEW, STATE_COMMITTING)) {
for (Supplier<? extends Mono<Void>> action : this.beforeCommitActions) {
mono = mono.then(action);
}
mono = mono.otherwise(ex -> {
// Ignore errors from beforeCommit actions
return Mono.empty();
});
mono = mono.then(() -> {
this.state.set(STATE_COMMITTED);
writeStatusCode();
writeHeaders();
writeCookies();
return Mono.empty();
});
}
return mono;
}
/**
* Implement this method to write to the underlying the response.
* @param body the publisher to write with
*/
protected abstract Mono<Void> writeWithInternal(Publisher<DataBuffer> body);
/**
* Implement this method to write the status code to the underlying response.
* This method is called once only.
*/
protected abstract void writeStatusCode();
/**
* Implement this method to apply header changes from {@link #getHeaders()}
* to the underlying response. This method is called once only.
*/
protected abstract void writeHeaders();
/**
* Implement this method to add cookies from {@link #getHeaders()} to the
* underlying response. This method is called once only.
*/
protected abstract void writeCookies();
@Override
public Mono<Void> setComplete() {
return applyBeforeCommit();
}
}

View File

@@ -0,0 +1,241 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.MonoSource;
import reactor.core.subscriber.SubscriberBarrier;
import reactor.core.util.EmptySubscription;
import org.springframework.util.Assert;
/**
* Given a write function that accepts a source {@code Publisher<T>} to write
* with and returns {@code Publisher<Void>} for the result, this operator helps
* to defer the invocation of the write function, until we know if the source
* publisher will begin publishing without an error. If the first emission is
* an error, the write function is bypassed, and the error is sent directly
* through the result publisher. Otherwise the write function is invoked.
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
*/
public class ChannelSendOperator<T> extends MonoSource<T, Void> {
private final Function<Publisher<T>, Publisher<Void>> writeFunction;
public ChannelSendOperator(Publisher<? extends T> source,
Function<Publisher<T>, Publisher<Void>> writeFunction) {
super(source);
this.writeFunction = writeFunction;
}
@Override
public void subscribe(Subscriber<? super Void> s) {
source.subscribe(new WriteWithBarrier(s));
}
private class WriteWithBarrier extends SubscriberBarrier<T, Void> implements Publisher<T> {
/**
* We've at at least one emission, we've called the write function, the write
* subscriber has subscribed and cached signals have been emitted to it.
* We're now simply passing data through to the write subscriber.
**/
private boolean readyToWrite = false;
/** No emission from upstream yet */
private boolean beforeFirstEmission = true;
/** Cached signal before readyToWrite */
private T item;
/** Cached 1st/2nd signal before readyToWrite */
private Throwable error;
/** Cached 1st/2nd signal before readyToWrite */
private boolean completed = false;
/** The actual writeSubscriber vs the downstream completion subscriber */
private Subscriber<? super T> writeSubscriber;
public WriteWithBarrier(Subscriber<? super Void> subscriber) {
super(subscriber);
}
@Override
protected void doOnSubscribe(Subscription subscription) {
super.doOnSubscribe(subscription);
super.upstream()
.request(1); // bypass doRequest
}
@Override
public void doNext(T item) {
if (this.readyToWrite) {
this.writeSubscriber.onNext(item);
return;
}
synchronized (this) {
if (this.readyToWrite) {
this.writeSubscriber.onNext(item);
}
else if (this.beforeFirstEmission) {
this.item = item;
this.beforeFirstEmission = false;
writeFunction.apply(this).subscribe(new DownstreamBridge(downstream()));
}
else {
subscription.cancel();
downstream().onError(new IllegalStateException("Unexpected item."));
}
}
}
@Override
public void doError(Throwable ex) {
if (this.readyToWrite) {
this.writeSubscriber.onError(ex);
return;
}
synchronized (this) {
if (this.readyToWrite) {
this.writeSubscriber.onError(ex);
}
else if (this.beforeFirstEmission) {
this.beforeFirstEmission = false;
downstream().onError(ex);
}
else {
this.error = ex;
}
}
}
@Override
public void doComplete() {
if (this.readyToWrite) {
this.writeSubscriber.onComplete();
return;
}
synchronized (this) {
if (this.readyToWrite) {
this.writeSubscriber.onComplete();
}
else if (this.beforeFirstEmission) {
this.completed = true;
this.beforeFirstEmission = false;
writeFunction.apply(this).subscribe(new DownstreamBridge(downstream()));
}
else {
this.completed = true;
}
}
}
@Override
public void subscribe(Subscriber<? super T> writeSubscriber) {
synchronized (this) {
Assert.isNull(this.writeSubscriber, "Only one writeSubscriber supported.");
this.writeSubscriber = writeSubscriber;
if (this.error != null || this.completed) {
this.writeSubscriber.onSubscribe(EmptySubscription.INSTANCE);
emitCachedSignals();
}
else {
this.writeSubscriber.onSubscribe(this);
}
}
}
/**
* Emit cached signals to the write subscriber.
* @return true if no more signals expected
*/
private boolean emitCachedSignals() {
if (this.item != null) {
this.writeSubscriber.onNext(this.item);
}
if (this.error != null) {
this.writeSubscriber.onError(this.error);
return true;
}
if (this.completed) {
this.writeSubscriber.onComplete();
return true;
}
return false;
}
@Override
protected void doRequest(long n) {
if (readyToWrite) {
super.doRequest(n);
return;
}
synchronized (this) {
if (this.writeSubscriber != null) {
readyToWrite = true;
if (emitCachedSignals()) {
return;
}
n--;
if (n == 0) {
return;
}
super.doRequest(n);
}
}
}
}
private class DownstreamBridge implements Subscriber<Void> {
private final Subscriber<? super Void> downstream;
public DownstreamBridge(Subscriber<? super Void> downstream) {
this.downstream = downstream;
}
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Void aVoid) {
}
@Override
public void onError(Throwable ex) {
this.downstream.onError(ex);
}
@Override
public void onComplete() {
this.downstream.onComplete();
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import reactor.core.publisher.Mono;
/**
* Contract for handling HTTP requests in a non-blocking way.
*
* @author Arjen Poutsma
*/
public interface HttpHandler {
/**
* Handle the given request and generate a response.
*
* @param request current HTTP request.
* @param response current HTTP response.
* @return {@code Mono<Void>} to indicate when request handling is complete.
*/
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}

View File

@@ -0,0 +1,52 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import io.netty.buffer.ByteBuf;
import reactor.core.publisher.Mono;
import reactor.io.ipc.ChannelHandler;
import reactor.io.netty.http.HttpChannel;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Stephane Maldini
*/
public class ReactorHttpHandlerAdapter
implements ChannelHandler<ByteBuf, ByteBuf, HttpChannel> {
private final HttpHandler httpHandler;
public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
Assert.notNull(httpHandler, "'httpHandler' is required.");
this.httpHandler = httpHandler;
}
@Override
public Mono<Void> apply(HttpChannel channel) {
NettyDataBufferFactory dataBufferFactory =
new NettyDataBufferFactory(channel.delegate().alloc());
ReactorServerHttpRequest adaptedRequest =
new ReactorServerHttpRequest(channel, dataBufferFactory);
ReactorServerHttpResponse adaptedResponse =
new ReactorServerHttpResponse(channel, dataBufferFactory);
return this.httpHandler.handle(adaptedRequest, adaptedResponse);
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import io.netty.handler.codec.http.cookie.Cookie;
import reactor.core.publisher.Flux;
import reactor.io.netty.http.HttpChannel;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the Reactor Net {@link HttpChannel}.
*
* @author Stephane Maldini
*/
public class ReactorServerHttpRequest extends AbstractServerHttpRequest {
private final HttpChannel channel;
private final NettyDataBufferFactory dataBufferFactory;
public ReactorServerHttpRequest(HttpChannel request,
NettyDataBufferFactory dataBufferFactory) {
Assert.notNull("'request' must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.channel = request;
this.dataBufferFactory = dataBufferFactory;
}
public HttpChannel getReactorChannel() {
return this.channel;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.channel.method().name());
}
@Override
protected URI initUri() throws URISyntaxException {
return new URI(this.channel.uri());
}
@Override
protected HttpHeaders initHeaders() {
HttpHeaders headers = new HttpHeaders();
for (String name : this.channel.headers().names()) {
headers.put(name, this.channel.headers().getAll(name));
}
return headers;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
for (CharSequence name : this.channel.cookies().keySet()) {
for (Cookie cookie : this.channel.cookies().get(name)) {
HttpCookie httpCookie = new HttpCookie(name.toString(), cookie.value());
cookies.add(name.toString(), httpCookie);
}
}
return cookies;
}
@Override
public Flux<DataBuffer> getBody() {
return this.channel.receive().retain().map(this.dataBufferFactory::wrap);
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.File;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.io.netty.http.HttpChannel;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.FlushingDataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Reactor Net {@link HttpChannel}.
*
* @author Stephane Maldini
* @author Rossen Stoyanchev
*/
public class ReactorServerHttpResponse extends AbstractServerHttpResponse
implements ZeroCopyHttpOutputMessage {
private final HttpChannel channel;
public ReactorServerHttpResponse(HttpChannel response,
DataBufferFactory dataBufferFactory) {
super(dataBufferFactory);
Assert.notNull("'response' must not be null.");
this.channel = response;
}
public HttpChannel getReactorChannel() {
return this.channel;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
getReactorChannel().status(HttpResponseStatus.valueOf(statusCode.value()));
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> publisher) {
return Flux.from(publisher)
.window()
.concatMap(w -> this.channel.send(w
.takeUntil(db -> db instanceof FlushingDataBuffer)
.map(this::toByteBuf)))
.then();
}
@Override
protected void writeHeaders() {
for (String name : getHeaders().keySet()) {
for (String value : getHeaders().get(name)) {
this.channel.responseHeaders().add(name, value);
}
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new DefaultCookie(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge(httpCookie.getMaxAge().getSeconds());
}
httpCookie.getDomain().ifPresent(cookie::setDomain);
httpCookie.getPath().ifPresent(cookie::setPath);
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.channel.addResponseCookie(cookie);
}
}
}
private ByteBuf toByteBuf(DataBuffer buffer) {
if (buffer instanceof NettyDataBuffer) {
return ((NettyDataBuffer) buffer).getNativeBuffer();
}
else {
return Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
}
@Override
public Mono<Void> writeWith(File file, long position, long count) {
return applyBeforeCommit().then(() -> {
return this.channel.sendFile(file, position, count);
});
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.util.BackpressureUtils;
/**
* Publisher returned from {@link ServerHttpResponse#writeWith(Publisher)}.
* @author Arjen Poutsma
*/
class ResponseBodyWriteResultPublisher implements Publisher<Void> {
private static final Log logger =
LogFactory.getLog(ResponseBodyWriteResultPublisher.class);
private final AtomicReference<State> state =
new AtomicReference<>(State.UNSUBSCRIBED);
private Subscriber<? super Void> subscriber;
private volatile boolean publisherCompleted;
private volatile Throwable publisherError;
@Override
public final void subscribe(Subscriber<? super Void> subscriber) {
if (logger.isTraceEnabled()) {
logger.trace(this.state + " subscribe: " + subscriber);
}
this.state.get().subscribe(this, subscriber);
}
private boolean changeState(State oldState, State newState) {
return this.state.compareAndSet(oldState, newState);
}
/**
* Publishes the complete signal to the subscriber of this publisher.
*/
public void publishComplete() {
if (logger.isTraceEnabled()) {
logger.trace(this.state + " publishComplete");
}
this.state.get().publishComplete(this);
}
/**
* Publishes the given error signal to the subscriber of this publisher.
*/
public void publishError(Throwable t) {
if (logger.isTraceEnabled()) {
logger.trace(this.state + " publishError: " + t);
}
this.state.get().publishError(this, t);
}
private static final class ResponseBodyWriteResultSubscription
implements Subscription {
private final ResponseBodyWriteResultPublisher publisher;
public ResponseBodyWriteResultSubscription(
ResponseBodyWriteResultPublisher publisher) {
this.publisher = publisher;
}
@Override
public final void request(long n) {
if (logger.isTraceEnabled()) {
logger.trace(state() + " request: " + n);
}
state().request(this.publisher, n);
}
@Override
public final void cancel() {
if (logger.isTraceEnabled()) {
logger.trace(state() + " cancel");
}
state().cancel(this.publisher);
}
private State state() {
return this.publisher.state.get();
}
}
private enum State {
UNSUBSCRIBED {
@Override
void subscribe(ResponseBodyWriteResultPublisher publisher,
Subscriber<? super Void> subscriber) {
Objects.requireNonNull(subscriber);
if (publisher.changeState(this, SUBSCRIBED)) {
Subscription subscription =
new ResponseBodyWriteResultSubscription(publisher);
publisher.subscriber = subscriber;
subscriber.onSubscribe(subscription);
if (publisher.publisherCompleted) {
publisher.publishComplete();
}
else if (publisher.publisherError != null) {
publisher.publishError(publisher.publisherError);
}
}
else {
throw new IllegalStateException(toString());
}
}
@Override
void publishComplete(ResponseBodyWriteResultPublisher publisher) {
publisher.publisherCompleted = true;
}
@Override
void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) {
publisher.publisherError = t;
}
},
SUBSCRIBED {
@Override
void request(ResponseBodyWriteResultPublisher publisher, long n) {
BackpressureUtils.checkRequest(n, publisher.subscriber);
}
@Override
void publishComplete(ResponseBodyWriteResultPublisher publisher) {
if (publisher.changeState(this, COMPLETED)) {
publisher.subscriber.onComplete();
}
}
@Override
void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) {
if (publisher.changeState(this, COMPLETED)) {
publisher.subscriber.onError(t);
}
}
},
COMPLETED {
@Override
void request(ResponseBodyWriteResultPublisher publisher, long n) {
// ignore
}
@Override
void cancel(ResponseBodyWriteResultPublisher publisher) {
// ignore
}
@Override
void publishComplete(ResponseBodyWriteResultPublisher publisher) {
// ignore
}
@Override
void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) {
// ignore
}
};
void subscribe(ResponseBodyWriteResultPublisher publisher,
Subscriber<? super Void> subscriber) {
throw new IllegalStateException(toString());
}
void request(ResponseBodyWriteResultPublisher publisher, long n) {
throw new IllegalStateException(toString());
}
void cancel(ResponseBodyWriteResultPublisher publisher) {
publisher.changeState(this, COMPLETED);
}
void publishComplete(ResponseBodyWriteResultPublisher publisher) {
throw new IllegalStateException(toString());
}
void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) {
throw new IllegalStateException(toString());
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import io.netty.buffer.ByteBuf;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import io.reactivex.netty.protocol.http.server.RequestHandler;
import org.reactivestreams.Publisher;
import reactor.core.converter.RxJava1ObservableConverter;
import rx.Observable;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Rossen Stoyanchev
*/
public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBuf> {
private final HttpHandler httpHandler;
public RxNettyHttpHandlerAdapter(HttpHandler httpHandler) {
Assert.notNull(httpHandler, "'httpHandler' is required");
this.httpHandler = httpHandler;
}
@Override
public Observable<Void> handle(HttpServerRequest<ByteBuf> request, HttpServerResponse<ByteBuf> response) {
NettyDataBufferFactory dataBufferFactory =
new NettyDataBufferFactory(response.unsafeNettyChannel().alloc());
RxNettyServerHttpRequest adaptedRequest =
new RxNettyServerHttpRequest(request, dataBufferFactory);
RxNettyServerHttpResponse adaptedResponse =
new RxNettyServerHttpResponse(response, dataBufferFactory);
Publisher<Void> result = this.httpHandler.handle(adaptedRequest, adaptedResponse);
return RxJava1ObservableConverter.fromPublisher(result);
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.cookie.Cookie;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import reactor.core.converter.RxJava1ObservableConverter;
import reactor.core.publisher.Flux;
import rx.Observable;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the RxNetty {@link HttpServerRequest}.
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
*/
public class RxNettyServerHttpRequest extends AbstractServerHttpRequest {
private final HttpServerRequest<ByteBuf> request;
private final NettyDataBufferFactory dataBufferFactory;
public RxNettyServerHttpRequest(HttpServerRequest<ByteBuf> request,
NettyDataBufferFactory dataBufferFactory) {
Assert.notNull("'request', request must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.dataBufferFactory = dataBufferFactory;
this.request = request;
}
public HttpServerRequest<ByteBuf> getRxNettyRequest() {
return this.request;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.request.getHttpMethod().name());
}
@Override
protected URI initUri() throws URISyntaxException {
return new URI(this.request.getUri());
}
@Override
protected HttpHeaders initHeaders() {
HttpHeaders headers = new HttpHeaders();
for (String name : this.request.getHeaderNames()) {
headers.put(name, this.request.getAllHeaderValues(name));
}
return headers;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
for (String name : this.request.getCookies().keySet()) {
for (Cookie cookie : this.request.getCookies().get(name)) {
HttpCookie httpCookie = new HttpCookie(name, cookie.value());
cookies.add(name, httpCookie);
}
}
return cookies;
}
@Override
public Flux<DataBuffer> getBody() {
Observable<DataBuffer> content = this.request.getContent().map(dataBufferFactory::wrap);
return RxJava1ObservableConverter.toPublisher(content);
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import org.reactivestreams.Publisher;
import reactor.core.converter.RxJava1ObservableConverter;
import reactor.core.publisher.Mono;
import rx.Observable;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.FlushingDataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the RxNetty {@link HttpServerResponse}.
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
*/
public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
private final HttpServerResponse<ByteBuf> response;
public RxNettyServerHttpResponse(HttpServerResponse<ByteBuf> response,
NettyDataBufferFactory dataBufferFactory) {
super(dataBufferFactory);
Assert.notNull("'response', response must not be null.");
this.response = response;
}
public HttpServerResponse<?> getRxNettyResponse() {
return this.response;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
this.response.setStatus(HttpResponseStatus.valueOf(statusCode.value()));
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> body) {
Observable<ByteBuf> content = RxJava1ObservableConverter.fromPublisher(body).map(this::toByteBuf);
return RxJava1ObservableConverter.toPublisher(this.response.write(content, bb -> bb instanceof FlushingByteBuf)).then();
}
private ByteBuf toByteBuf(DataBuffer buffer) {
ByteBuf byteBuf = (buffer instanceof NettyDataBuffer ? ((NettyDataBuffer) buffer).getNativeBuffer() : Unpooled.wrappedBuffer(buffer.asByteBuffer()));
return (buffer instanceof FlushingDataBuffer ? new FlushingByteBuf(byteBuf) : byteBuf);
}
@Override
protected void writeHeaders() {
for (String name : getHeaders().keySet()) {
for (String value : getHeaders().get(name))
this.response.addHeader(name, value);
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new DefaultCookie(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge(httpCookie.getMaxAge().getSeconds());
}
httpCookie.getDomain().ifPresent(cookie::setDomain);
httpCookie.getPath().ifPresent(cookie::setPath);
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.response.addCookie(cookie);
}
}
}
private class FlushingByteBuf extends CompositeByteBuf {
public FlushingByteBuf(ByteBuf byteBuf) {
super(byteBuf.alloc(), byteBuf.isDirect(), 1);
this.addComponent(true, byteBuf);
}
}
/*
While the underlying implementation of {@link ZeroCopyHttpOutputMessage} seems to
work; it does bypass {@link #applyBeforeCommit} and more importantly it doesn't change
its {@linkplain #state()). Therefore it's commented out, for now.
We should revisit this code once
https://github.com/ReactiveX/RxNetty/issues/194 has been fixed.
@Override
public Mono<Void> writeWith(File file, long position, long count) {
Channel channel = this.response.unsafeNettyChannel();
HttpResponse httpResponse =
new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
io.netty.handler.codec.http.HttpHeaders headers = httpResponse.headers();
for (Map.Entry<String, List<String>> header : getHeaders().entrySet()) {
String headerName = header.getKey();
for (String headerValue : header.getValue()) {
headers.add(headerName, headerValue);
}
}
Mono<Void> responseWrite = MonoChannelFuture.from(channel.write(httpResponse));
FileRegion fileRegion = new DefaultFileRegion(file, position, count);
Mono<Void> fileWrite = MonoChannelFuture.from(channel.writeAndFlush(fileRegion));
return Flux.concat(applyBeforeCommit(), responseWrite, fileWrite).then();
}
*/
}

View File

@@ -0,0 +1,41 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpRequest;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.util.MultiValueMap;
/**
* Represents a reactive server-side HTTP request
*
* @author Arjen Poutsma
*/
public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
/**
* Return a read-only map with parsed and decoded query parameter values.
*/
MultiValueMap<String, String> getQueryParams();
/**
* Return a read-only map of cookies sent by the client.
*/
MultiValueMap<String, HttpCookie> getCookies();
}

View File

@@ -0,0 +1,63 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ResponseCookie;
import org.springframework.util.MultiValueMap;
/**
* Represents a reactive server-side HTTP response.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
*/
public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
/**
* Set the HTTP status code of the response.
* @param status the HTTP status as an {@link HttpStatus} enum value
* @return {@code false} if the status code has not been set because the HTTP response
* is already committed, {@code true} if it has been set correctly.
*/
boolean setStatusCode(HttpStatus status);
/**
* Return the HTTP status code or {@code null} if not set.
*/
HttpStatus getStatusCode();
/**
* Return a mutable map with the cookies to send to the server.
*/
MultiValueMap<String, ResponseCookie> getCookies();
/**
* Indicate that request handling is complete, allowing for any cleanup or
* end-of-processing tasks to be performed such as applying header changes
* made via {@link #getHeaders()} to the underlying server response (if not
* applied already).
* <p>This method should be automatically invoked at the end of request
* processing so typically applications should not have to invoke it.
* If invoked multiple times it should have no side effects.
*/
Mono<Void> setComplete();
}

View File

@@ -0,0 +1,125 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
@WebServlet(asyncSupported = true)
@SuppressWarnings("serial")
public class ServletHttpHandlerAdapter extends HttpServlet {
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class);
private HttpHandler handler;
// Servlet is based on blocking I/O, hence the usage of non-direct, heap-based buffers
// (i.e. 'false' as constructor argument)
private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false);
private int bufferSize = DEFAULT_BUFFER_SIZE;
public void setHandler(HttpHandler handler) {
Assert.notNull(handler, "'handler' must not be null");
this.handler = handler;
}
public void setDataBufferFactory(DataBufferFactory dataBufferFactory) {
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.dataBufferFactory = dataBufferFactory;
}
public void setBufferSize(int bufferSize) {
Assert.isTrue(bufferSize > 0);
this.bufferSize = bufferSize;
}
@Override
protected void service(HttpServletRequest servletRequest,
HttpServletResponse servletResponse) throws ServletException, IOException {
AsyncContext asyncContext = servletRequest.startAsync();
ServletServerHttpRequest request =
new ServletServerHttpRequest(servletRequest, this.dataBufferFactory,
this.bufferSize);
ServletServerHttpResponse response =
new ServletServerHttpResponse(servletResponse, this.dataBufferFactory,
this.bufferSize);
HandlerResultSubscriber resultSubscriber =
new HandlerResultSubscriber(asyncContext);
this.handler.handle(request, response).subscribe(resultSubscriber);
}
private static class HandlerResultSubscriber implements Subscriber<Void> {
private final AsyncContext asyncContext;
public HandlerResultSubscriber(AsyncContext asyncContext) {
this.asyncContext = asyncContext;
}
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Void aVoid) {
// no op
}
@Override
public void onError(Throwable ex) {
logger.error("Error from request handling. Completing the request.", ex);
HttpServletResponse response =
(HttpServletResponse) this.asyncContext.getResponse();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
this.asyncContext.complete();
}
@Override
public void onComplete() {
this.asyncContext.complete();
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.Map;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}.
* @author Rossen Stoyanchev
*/
public class ServletServerHttpRequest extends AbstractServerHttpRequest {
private final Object bodyPublisherMonitor = new Object();
private volatile RequestBodyPublisher bodyPublisher;
private final HttpServletRequest request;
private final DataBufferFactory dataBufferFactory;
private final int bufferSize;
public ServletServerHttpRequest(HttpServletRequest request,
DataBufferFactory dataBufferFactory, int bufferSize) {
Assert.notNull(request, "'request' must not be null.");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
Assert.isTrue(bufferSize > 0);
this.request = request;
this.dataBufferFactory = dataBufferFactory;
this.bufferSize = bufferSize;
}
public HttpServletRequest getServletRequest() {
return this.request;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(getServletRequest().getMethod());
}
@Override
protected URI initUri() throws URISyntaxException {
StringBuffer url = this.request.getRequestURL();
String query = this.request.getQueryString();
if (StringUtils.hasText(query)) {
url.append('?').append(query);
}
return new URI(url.toString());
}
@Override
protected HttpHeaders initHeaders() {
HttpHeaders headers = new HttpHeaders();
for (Enumeration<?> names = getServletRequest().getHeaderNames();
names.hasMoreElements(); ) {
String name = (String) names.nextElement();
for (Enumeration<?> values = getServletRequest().getHeaders(name);
values.hasMoreElements(); ) {
headers.add(name, (String) values.nextElement());
}
}
MediaType contentType = headers.getContentType();
if (contentType == null) {
String requestContentType = getServletRequest().getContentType();
if (StringUtils.hasLength(requestContentType)) {
contentType = MediaType.parseMediaType(requestContentType);
headers.setContentType(contentType);
}
}
if (contentType != null && contentType.getCharset() == null) {
String encoding = getServletRequest().getCharacterEncoding();
if (StringUtils.hasLength(encoding)) {
Charset charset = Charset.forName(encoding);
Map<String, String> params = new LinkedCaseInsensitiveMap<>();
params.putAll(contentType.getParameters());
params.put("charset", charset.toString());
headers.setContentType(
new MediaType(contentType.getType(), contentType.getSubtype(),
params));
}
}
if (headers.getContentLength() == -1) {
int contentLength = getServletRequest().getContentLength();
if (contentLength != -1) {
headers.setContentLength(contentLength);
}
}
return headers;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> httpCookies = new LinkedMultiValueMap<>();
Cookie[] cookies = this.request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
HttpCookie httpCookie = new HttpCookie(name, cookie.getValue());
httpCookies.add(name, httpCookie);
}
}
return httpCookies;
}
@Override
public Flux<DataBuffer> getBody() {
try {
RequestBodyPublisher bodyPublisher = this.bodyPublisher;
if (bodyPublisher == null) {
synchronized (this.bodyPublisherMonitor) {
bodyPublisher = this.bodyPublisher;
if (bodyPublisher == null) {
this.bodyPublisher = bodyPublisher = createBodyPublisher();
}
}
}
return Flux.from(bodyPublisher);
}
catch (IOException ex) {
return Flux.error(ex);
}
}
private RequestBodyPublisher createBodyPublisher() throws IOException {
RequestBodyPublisher bodyPublisher =
new RequestBodyPublisher(request.getInputStream(), this.dataBufferFactory,
this.bufferSize);
bodyPublisher.registerListener();
return bodyPublisher;
}
private static class RequestBodyPublisher extends AbstractRequestBodyPublisher {
private final RequestBodyPublisher.RequestBodyReadListener readListener =
new RequestBodyPublisher.RequestBodyReadListener();
private final ServletInputStream inputStream;
private final DataBufferFactory dataBufferFactory;
private final byte[] buffer;
public RequestBodyPublisher(ServletInputStream inputStream,
DataBufferFactory dataBufferFactory, int bufferSize) {
this.inputStream = inputStream;
this.dataBufferFactory = dataBufferFactory;
this.buffer = new byte[bufferSize];
}
public void registerListener() throws IOException {
this.inputStream.setReadListener(this.readListener);
}
@Override
protected void checkOnDataAvailable() {
if (!this.inputStream.isFinished() && this.inputStream.isReady()) {
onDataAvailable();
}
}
@Override
protected DataBuffer read() throws IOException {
if (this.inputStream.isReady()) {
int read = this.inputStream.read(this.buffer);
if (logger.isTraceEnabled()) {
logger.trace("read:" + read);
}
if (read > 0) {
DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(read);
dataBuffer.write(this.buffer, 0, read);
return dataBuffer;
}
}
return null;
}
private class RequestBodyReadListener implements ReadListener {
@Override
public void onDataAvailable() throws IOException {
RequestBodyPublisher.this.onDataAvailable();
}
@Override
public void onAllDataRead() throws IOException {
RequestBodyPublisher.this.onAllDataRead();
}
@Override
public void onError(Throwable throwable) {
RequestBodyPublisher.this.onError(throwable);
}
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}.
* @author Rossen Stoyanchev
*/
public class ServletServerHttpResponse extends AbstractServerHttpResponse {
private final Object bodyProcessorMonitor = new Object();
private volatile ResponseBodyProcessor bodyProcessor;
private final HttpServletResponse response;
private final int bufferSize;
public ServletServerHttpResponse(HttpServletResponse response,
DataBufferFactory dataBufferFactory, int bufferSize) throws IOException {
super(dataBufferFactory);
Assert.notNull(response, "'response' must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
Assert.isTrue(bufferSize > 0);
this.response = response;
this.bufferSize = bufferSize;
}
public HttpServletResponse getServletResponse() {
return this.response;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
getServletResponse().setStatus(statusCode.value());
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> publisher) {
Assert.state(this.bodyProcessor == null,
"Response body publisher is already provided");
try {
synchronized (this.bodyProcessorMonitor) {
if (this.bodyProcessor == null) {
this.bodyProcessor = createBodyProcessor();
}
else {
throw new IllegalStateException(
"Response body publisher is already provided");
}
}
return Mono.from(subscriber -> {
publisher.subscribe(this.bodyProcessor);
this.bodyProcessor.subscribe(subscriber);
});
}
catch (IOException ex) {
return Mono.error(ex);
}
}
private ResponseBodyProcessor createBodyProcessor() throws IOException {
ResponseBodyProcessor bodyProcessor =
new ResponseBodyProcessor(this.response.getOutputStream(),
this.bufferSize);
bodyProcessor.registerListener();
return bodyProcessor;
}
@Override
protected void writeHeaders() {
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
String headerName = entry.getKey();
for (String headerValue : entry.getValue()) {
this.response.addHeader(headerName, headerValue);
}
}
MediaType contentType = getHeaders().getContentType();
if (this.response.getContentType() == null && contentType != null) {
this.response.setContentType(contentType.toString());
}
Charset charset = (contentType != null ? contentType.getCharset() : null);
if (this.response.getCharacterEncoding() == null && charset != null) {
this.response.setCharacterEncoding(charset.name());
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new Cookie(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
httpCookie.getDomain().ifPresent(cookie::setDomain);
httpCookie.getPath().ifPresent(cookie::setPath);
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.response.addCookie(cookie);
}
}
}
private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor {
private final ResponseBodyWriteListener writeListener =
new ResponseBodyWriteListener();
private final ServletOutputStream outputStream;
private final int bufferSize;
private volatile boolean flushOnNext;
public ResponseBodyProcessor(ServletOutputStream outputStream, int bufferSize) {
this.outputStream = outputStream;
this.bufferSize = bufferSize;
}
public void registerListener() throws IOException {
this.outputStream.setWriteListener(this.writeListener);
}
@Override
protected boolean isWritePossible() {
return this.outputStream.isReady();
}
@Override
protected boolean write(DataBuffer dataBuffer) throws IOException {
if (this.flushOnNext) {
flush();
}
boolean ready = this.outputStream.isReady();
if (this.logger.isTraceEnabled()) {
this.logger.trace("write: " + dataBuffer + " ready: " + ready);
}
if (ready) {
int total = dataBuffer.readableByteCount();
int written = writeDataBuffer(dataBuffer);
if (this.logger.isTraceEnabled()) {
this.logger.trace("written: " + written + " total: " + total);
}
return written == total;
}
else {
return false;
}
}
@Override
protected void flush() throws IOException {
if (this.outputStream.isReady()) {
if (logger.isTraceEnabled()) {
logger.trace("flush");
}
try {
this.outputStream.flush();
this.flushOnNext = false;
return;
}
catch (IOException ignored) {
}
}
this.flushOnNext = true;
}
private int writeDataBuffer(DataBuffer dataBuffer) throws IOException {
InputStream input = dataBuffer.asInputStream();
int bytesWritten = 0;
byte[] buffer = new byte[this.bufferSize];
int bytesRead = -1;
while (this.outputStream.isReady() &&
(bytesRead = input.read(buffer)) != -1) {
this.outputStream.write(buffer, 0, bytesRead);
bytesWritten += bytesRead;
}
return bytesWritten;
}
private class ResponseBodyWriteListener implements WriteListener {
@Override
public void onWritePossible() throws IOException {
ResponseBodyProcessor.this.onWritePossible();
}
@Override
public void onError(Throwable ex) {
// Error on writing to the HTTP stream, so any further writes will probably
// fail. Let's log instead of calling {@link #writeError}.
ResponseBodyProcessor.this.logger
.error("ResponseBodyWriteListener error", ex);
}
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
* @author Arjen Poutsma
*/
public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler {
private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class);
private final HttpHandler delegate;
private final DataBufferFactory dataBufferFactory;
public UndertowHttpHandlerAdapter(HttpHandler delegate,
DataBufferFactory dataBufferFactory) {
Assert.notNull(delegate, "'delegate' is required");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.delegate = delegate;
this.dataBufferFactory = dataBufferFactory;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
ServerHttpRequest request =
new UndertowServerHttpRequest(exchange, this.dataBufferFactory);
ServerHttpResponse response =
new UndertowServerHttpResponse(exchange, this.dataBufferFactory);
this.delegate.handle(request, response).subscribe(new Subscriber<Void>() {
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Void aVoid) {
// no op
}
@Override
public void onError(Throwable ex) {
logger.error("Error from request handling. Completing the request.", ex);
if (!exchange.isResponseStarted() && exchange.getStatusCode() <= 500) {
exchange.setStatusCode(500);
}
exchange.endExchange();
}
@Override
public void onComplete() {
exchange.endExchange();
}
});
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import io.undertow.connector.PooledByteBuffer;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.util.HeaderValues;
import org.xnio.ChannelListener;
import org.xnio.channels.StreamSourceChannel;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}.
*
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
*/
public class UndertowServerHttpRequest extends AbstractServerHttpRequest {
private final HttpServerExchange exchange;
private final RequestBodyPublisher body;
public UndertowServerHttpRequest(HttpServerExchange exchange,
DataBufferFactory dataBufferFactory) {
Assert.notNull(exchange, "'exchange' is required.");
this.exchange = exchange;
this.body = new RequestBodyPublisher(exchange, dataBufferFactory);
this.body.registerListener();
}
public HttpServerExchange getUndertowExchange() {
return this.exchange;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.getUndertowExchange().getRequestMethod().toString());
}
@Override
protected URI initUri() throws URISyntaxException {
return new URI(this.exchange.getRequestScheme(), null,
this.exchange.getHostName(), this.exchange.getHostPort(),
this.exchange.getRequestURI(), this.exchange.getQueryString(), null);
}
@Override
protected HttpHeaders initHeaders() {
HttpHeaders headers = new HttpHeaders();
for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) {
headers.put(values.getHeaderName().toString(), values);
}
return headers;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
for (String name : this.exchange.getRequestCookies().keySet()) {
Cookie cookie = this.exchange.getRequestCookies().get(name);
HttpCookie httpCookie = new HttpCookie(name, cookie.getValue());
cookies.add(name, httpCookie);
}
return cookies;
}
@Override
public Flux<DataBuffer> getBody() {
return Flux.from(this.body);
}
private static class RequestBodyPublisher extends AbstractRequestBodyPublisher {
private final ChannelListener<StreamSourceChannel> readListener =
new ReadListener();
private final ChannelListener<StreamSourceChannel> closeListener =
new CloseListener();
private final StreamSourceChannel requestChannel;
private final DataBufferFactory dataBufferFactory;
private final PooledByteBuffer pooledByteBuffer;
public RequestBodyPublisher(HttpServerExchange exchange,
DataBufferFactory dataBufferFactory) {
this.requestChannel = exchange.getRequestChannel();
this.pooledByteBuffer =
exchange.getConnection().getByteBufferPool().allocate();
this.dataBufferFactory = dataBufferFactory;
}
private void registerListener() {
this.requestChannel.getReadSetter().set(this.readListener);
this.requestChannel.getCloseSetter().set(this.closeListener);
this.requestChannel.resumeReads();
}
@Override
protected void checkOnDataAvailable() {
onDataAvailable();
}
@Override
protected DataBuffer read() throws IOException {
ByteBuffer byteBuffer = this.pooledByteBuffer.getBuffer();
int read = this.requestChannel.read(byteBuffer);
if (logger.isTraceEnabled()) {
logger.trace("read:" + read);
}
if (read > 0) {
byteBuffer.flip();
return this.dataBufferFactory.wrap(byteBuffer);
}
else if (read == -1) {
onAllDataRead();
}
return null;
}
private class ReadListener implements ChannelListener<StreamSourceChannel> {
@Override
public void handleEvent(StreamSourceChannel channel) {
onDataAvailable();
}
}
private class CloseListener implements ChannelListener<StreamSourceChannel> {
@Override
public void handleEvent(StreamSourceChannel channel) {
onAllDataRead();
}
}
}
}

View File

@@ -0,0 +1,231 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.List;
import java.util.Map;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import io.undertow.util.HttpString;
import org.reactivestreams.Publisher;
import org.xnio.ChannelListener;
import org.xnio.channels.StreamSinkChannel;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}.
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
* @author Arjen Poutsma
*/
public class UndertowServerHttpResponse extends AbstractServerHttpResponse
implements ZeroCopyHttpOutputMessage {
private final Object bodyProcessorMonitor = new Object();
private volatile ResponseBodyProcessor bodyProcessor;
private final HttpServerExchange exchange;
public UndertowServerHttpResponse(HttpServerExchange exchange,
DataBufferFactory dataBufferFactory) {
super(dataBufferFactory);
Assert.notNull(exchange, "'exchange' is required.");
this.exchange = exchange;
}
public HttpServerExchange getUndertowExchange() {
return this.exchange;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
getUndertowExchange().setStatusCode(statusCode.value());
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> publisher) {
Assert.state(this.bodyProcessor == null,
"Response body publisher is already provided");
try {
synchronized (this.bodyProcessorMonitor) {
if (this.bodyProcessor == null) {
this.bodyProcessor = createBodyProcessor();
}
else {
throw new IllegalStateException(
"Response body publisher is already provided");
}
}
return Mono.from(subscriber -> {
publisher.subscribe(this.bodyProcessor);
this.bodyProcessor.subscribe(subscriber);
});
}
catch (IOException ex) {
return Mono.error(ex);
}
}
private ResponseBodyProcessor createBodyProcessor() throws IOException {
ResponseBodyProcessor bodyProcessor = new ResponseBodyProcessor(this.exchange);
bodyProcessor.registerListener();
return bodyProcessor;
}
@Override
public Mono<Void> writeWith(File file, long position, long count) {
writeHeaders();
writeCookies();
try {
StreamSinkChannel responseChannel =
getUndertowExchange().getResponseChannel();
FileChannel in = new FileInputStream(file).getChannel();
long result = responseChannel.transferFrom(in, position, count);
if (result < count) {
return Mono.error(new IOException(
"Could only write " + result + " out of " + count + " bytes"));
}
else {
return Mono.empty();
}
}
catch (IOException ex) {
return Mono.error(ex);
}
}
@Override
protected void writeHeaders() {
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
HttpString headerName = HttpString.tryFromString(entry.getKey());
this.exchange.getResponseHeaders().addAll(headerName, entry.getValue());
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
httpCookie.getDomain().ifPresent(cookie::setDomain);
httpCookie.getPath().ifPresent(cookie::setPath);
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.exchange.getResponseCookies().putIfAbsent(name, cookie);
}
}
}
private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor {
private final ChannelListener<StreamSinkChannel> listener = new WriteListener();
private final StreamSinkChannel responseChannel;
private volatile ByteBuffer byteBuffer;
public ResponseBodyProcessor(HttpServerExchange exchange) {
this.responseChannel = exchange.getResponseChannel();
}
public void registerListener() {
this.responseChannel.getWriteSetter().set(this.listener);
this.responseChannel.resumeWrites();
}
@Override
protected void flush() throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("flush");
}
this.responseChannel.flush();
}
@Override
protected boolean write(DataBuffer dataBuffer) throws IOException {
if (this.byteBuffer == null) {
return false;
}
if (logger.isTraceEnabled()) {
logger.trace("write: " + dataBuffer);
}
int total = this.byteBuffer.remaining();
int written = writeByteBuffer(this.byteBuffer);
if (logger.isTraceEnabled()) {
logger.trace("written: " + written + " total: " + total);
}
return written == total;
}
private int writeByteBuffer(ByteBuffer byteBuffer) throws IOException {
int written;
int totalWritten = 0;
do {
written = this.responseChannel.write(byteBuffer);
totalWritten += written;
}
while (byteBuffer.hasRemaining() && written > 0);
return totalWritten;
}
@Override
protected void receiveBuffer(DataBuffer dataBuffer) {
super.receiveBuffer(dataBuffer);
this.byteBuffer = dataBuffer.asByteBuffer();
}
@Override
protected void releaseBuffer() {
super.releaseBuffer();
this.byteBuffer = null;
}
private class WriteListener implements ChannelListener<StreamSinkChannel> {
@Override
public void handleEvent(StreamSinkChannel channel) {
onWritePossible();
}
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Core package of the reactive server-side HTTP support.
*/
package org.springframework.http.server.reactive;

View File

@@ -0,0 +1,96 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import java.net.URI;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
/**
* Holds all the application information required to build an actual HTTP client request.
* <p>The request body is materialized by a {@code Publisher} of Objects and their type
* by a {@code ResolvableType} instance; it should be later converted to a
* {@code Publisher<DataBuffer>} to be written to the actual HTTP client request.
*
* @author Brian Clozel
*/
public class ClientWebRequest {
protected final HttpMethod httpMethod;
protected final URI url;
protected HttpHeaders httpHeaders;
private MultiValueMap<String, HttpCookie> cookies;
protected Publisher<?> body;
protected ResolvableType elementType;
public ClientWebRequest(HttpMethod httpMethod, URI url) {
this.httpMethod = httpMethod;
this.url = url;
}
public HttpMethod getMethod() {
return httpMethod;
}
public URI getUrl() {
return url;
}
public HttpHeaders getHttpHeaders() {
return httpHeaders;
}
public void setHttpHeaders(HttpHeaders httpHeaders) {
this.httpHeaders = httpHeaders;
}
public MultiValueMap<String, HttpCookie> getCookies() {
return cookies;
}
public void setCookies(MultiValueMap<String, HttpCookie> cookies) {
this.cookies = cookies;
}
public Publisher<?> getBody() {
return body;
}
public void setBody(Publisher<?> body) {
this.body = body;
}
public ResolvableType getElementType() {
return elementType;
}
public void setElementType(ResolvableType elementType) {
this.elementType = elementType;
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
/**
* Build {@link ClientWebRequest}s
*
* @author Brian Clozel
*/
public interface ClientWebRequestBuilder {
ClientWebRequest build();
}

View File

@@ -0,0 +1,111 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import org.springframework.http.HttpMethod;
/**
* Static factory methods for {@link DefaultClientWebRequestBuilder ClientWebRequestBuilders}
*
* @author Brian Clozel
*/
public abstract class ClientWebRequestBuilders {
/**
* Create a {@link DefaultClientWebRequestBuilder} for a GET request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder get(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for a POST request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder post(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for a PUT request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder put(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for a PATCH request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder patch(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for a DELETE request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder delete(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for an OPTIONS request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder options(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for a HEAD request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder head(String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables);
}
/**
* Create a {@link DefaultClientWebRequestBuilder} for a request with the given HTTP method.
*
* @param httpMethod the HTTP method
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static DefaultClientWebRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) {
return new DefaultClientWebRequestBuilder(httpMethod, urlTemplate, urlVariables);
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
/**
* Allow post processing and/or wrapping the {@link ClientWebRequest} before
* it's sent to the origin server.
*
* @author Rob Winch
* @author Brian Clozel
* @see DefaultClientWebRequestBuilder#apply(ClientWebRequestPostProcessor)
*/
public interface ClientWebRequestPostProcessor {
/**
* Implementations can modify and/or wrap the {@link ClientWebRequest} passed in
* and return it
*
* @param request the {@link ClientWebRequest} to be modified and/or wrapped.
*/
ClientWebRequest postProcess(ClientWebRequest request);
}

View File

@@ -0,0 +1,196 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.DefaultUriTemplateHandler;
import org.springframework.web.util.UriTemplateHandler;
/**
* Builds a {@link ClientHttpRequest} using a {@link Publisher}
* as request body.
*
* <p>See static factory methods in {@link ClientWebRequestBuilders}
*
* @author Brian Clozel
* @see ClientWebRequestBuilders
*/
public class DefaultClientWebRequestBuilder implements ClientWebRequestBuilder {
private final UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();
private HttpMethod httpMethod;
private HttpHeaders httpHeaders;
private URI url;
private final MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
private Publisher<?> body;
private ResolvableType elementType;
private List<ClientWebRequestPostProcessor> postProcessors = new ArrayList<>();
protected DefaultClientWebRequestBuilder() {
}
public DefaultClientWebRequestBuilder(HttpMethod httpMethod, String urlTemplate,
Object... urlVariables) {
this.httpMethod = httpMethod;
this.httpHeaders = new HttpHeaders();
this.url = this.uriTemplateHandler.expand(urlTemplate, urlVariables);
}
public DefaultClientWebRequestBuilder(HttpMethod httpMethod, URI url) {
this.httpMethod = httpMethod;
this.httpHeaders = new HttpHeaders();
this.url = url;
}
/**
* Add an HTTP request header
*/
public DefaultClientWebRequestBuilder header(String name, String... values) {
Arrays.stream(values).forEach(value -> this.httpHeaders.add(name, value));
return this;
}
/**
* Add all provided HTTP request headers
*/
public DefaultClientWebRequestBuilder headers(HttpHeaders httpHeaders) {
this.httpHeaders = httpHeaders;
return this;
}
/**
* Set the Content-Type request header to the given {@link MediaType}
*/
public DefaultClientWebRequestBuilder contentType(MediaType contentType) {
this.httpHeaders.setContentType(contentType);
return this;
}
/**
* Set the Content-Type request header to the given media type
*/
public DefaultClientWebRequestBuilder contentType(String contentType) {
this.httpHeaders.setContentType(MediaType.parseMediaType(contentType));
return this;
}
/**
* Set the Accept request header to the given {@link MediaType}s
*/
public DefaultClientWebRequestBuilder accept(MediaType... mediaTypes) {
this.httpHeaders.setAccept(Arrays.asList(mediaTypes));
return this;
}
/**
* Set the Accept request header to the given media types
*/
public DefaultClientWebRequestBuilder accept(String... mediaTypes) {
this.httpHeaders.setAccept(
Arrays.stream(mediaTypes).map(type -> MediaType.parseMediaType(type))
.collect(Collectors.toList()));
return this;
}
/**
* Add a Cookie to the HTTP request
*/
public DefaultClientWebRequestBuilder cookie(String name, String value) {
return cookie(new HttpCookie(name, value));
}
/**
* Add a Cookie to the HTTP request
*/
public DefaultClientWebRequestBuilder cookie(HttpCookie cookie) {
this.cookies.add(cookie.getName(), cookie);
return this;
}
/**
* Allows performing more complex operations with a strategy. For example, a
* {@link ClientWebRequestPostProcessor} implementation might accept the arguments of username
* and password and set an HTTP Basic authentication header.
*
* @param postProcessor the {@link ClientWebRequestPostProcessor} to use. Cannot be null.
*
* @return this instance for further modifications.
*/
public DefaultClientWebRequestBuilder apply(ClientWebRequestPostProcessor postProcessor) {
Assert.notNull(postProcessor, "`postProcessor` is required");
this.postProcessors.add(postProcessor);
return this;
}
/**
* Use the given object as the request body
*/
public DefaultClientWebRequestBuilder body(Object content) {
this.body = Mono.just(content);
this.elementType = ResolvableType.forInstance(content);
return this;
}
/**
* Use the given {@link Publisher} as the request body and use its {@link ResolvableType}
* as type information for the element published by this reactive stream
*/
public DefaultClientWebRequestBuilder body(Publisher<?> content, ResolvableType publisherType) {
this.body = content;
this.elementType = publisherType;
return this;
}
@Override
public ClientWebRequest build() {
ClientWebRequest clientWebRequest = new ClientWebRequest(this.httpMethod, this.url);
clientWebRequest.setHttpHeaders(this.httpHeaders);
clientWebRequest.setCookies(this.cookies);
clientWebRequest.setBody(this.body);
clientWebRequest.setElementType(this.elementType);
for (ClientWebRequestPostProcessor postProcessor : this.postProcessors) {
clientWebRequest = postProcessor.postProcess(clientWebRequest);
}
return clientWebRequest;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
/**
* A {@code ResponseExtractor} extracts the relevant part of a
* raw {@link org.springframework.http.client.reactive.ClientHttpResponse},
* optionally decoding the response body and using a target composition API.
*
* <p>See static factory methods in {@link ResponseExtractors}.
*
* @author Brian Clozel
*/
public interface ResponseExtractor<T> {
T extract(Mono<ClientHttpResponse> clientResponse, List<HttpMessageConverter<?>> messageConverters);
}

View File

@@ -0,0 +1,163 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import java.util.List;
import java.util.Optional;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Static factory methods for {@link ResponseExtractor} based on the {@link Flux} and
* {@link Mono} API.
*
* @author Brian Clozel
*/
public class ResponseExtractors {
private static final Object EMPTY_BODY = new Object();
/**
* Extract the response body and decode it, returning it as a {@code Mono<T>}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
@SuppressWarnings("unchecked")
public static <T> ResponseExtractor<Mono<T>> body(ResolvableType bodyType) {
return (clientResponse, messageConverters) -> (Mono<T>) clientResponse
.flatMap(resp -> decodeResponseBody(resp, bodyType,
messageConverters))
.next();
}
/**
* Extract the response body and decode it, returning it as a {@code Mono<T>}
*/
public static <T> ResponseExtractor<Mono<T>> body(Class<T> sourceClass) {
ResolvableType bodyType = ResolvableType.forClass(sourceClass);
return body(bodyType);
}
/**
* Extract the response body and decode it, returning it as a {@code Flux<T>}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
public static <T> ResponseExtractor<Flux<T>> bodyStream(ResolvableType bodyType) {
return (clientResponse, messageConverters) -> clientResponse
.flatMap(resp -> decodeResponseBody(resp, bodyType, messageConverters));
}
/**
* Extract the response body and decode it, returning it as a {@code Flux<T>}
*/
public static <T> ResponseExtractor<Flux<T>> bodyStream(Class<T> sourceClass) {
ResolvableType bodyType = ResolvableType.forClass(sourceClass);
return bodyStream(bodyType);
}
/**
* Extract the full response body as a {@code ResponseEntity} with its body decoded as
* a single type {@code T}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
@SuppressWarnings("unchecked")
public static <T> ResponseExtractor<Mono<ResponseEntity<T>>> response(
ResolvableType bodyType) {
return (clientResponse, messageConverters) -> clientResponse.then(response -> {
return Mono.when(
decodeResponseBody(response, bodyType,
messageConverters).next().defaultIfEmpty(
EMPTY_BODY),
Mono.just(response.getHeaders()),
Mono.just(response.getStatusCode()));
}).map(tuple -> {
Object body = (tuple.getT1() != EMPTY_BODY ? tuple.getT1() : null);
return new ResponseEntity<>((T) body, tuple.getT2(), tuple.getT3());
});
}
/**
* Extract the full response body as a {@code ResponseEntity} with its body decoded as
* a single type {@code T}
*/
public static <T> ResponseExtractor<Mono<ResponseEntity<T>>> response(
Class<T> bodyClass) {
ResolvableType bodyType = ResolvableType.forClass(bodyClass);
return response(bodyType);
}
/**
* Extract the full response body as a {@code ResponseEntity} with its body decoded as
* a {@code Flux<T>}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
public static <T> ResponseExtractor<Mono<ResponseEntity<Flux<T>>>> responseStream(
ResolvableType type) {
return (clientResponse, messageConverters) -> clientResponse
.map(response -> new ResponseEntity<>(
decodeResponseBody(response, type,
messageConverters),
response.getHeaders(), response.getStatusCode()));
}
/**
* Extract the full response body as a {@code ResponseEntity} with its body decoded as
* a {@code Flux<T>}
*/
public static <T> ResponseExtractor<Mono<ResponseEntity<Flux<T>>>> responseStream(
Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return responseStream(resolvableType);
}
/**
* Extract the response headers as an {@code HttpHeaders} instance
*/
public static ResponseExtractor<Mono<HttpHeaders>> headers() {
return (clientResponse, messageConverters) -> clientResponse.map(resp -> resp.getHeaders());
}
@SuppressWarnings("unchecked")
protected static <T> Flux<T> decodeResponseBody(ClientHttpResponse response,
ResolvableType responseType,
List<HttpMessageConverter<?>> messageConverters) {
MediaType contentType = response.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> converter = resolveConverter(messageConverters,
responseType, contentType);
if (!converter.isPresent()) {
return Flux.error(new IllegalStateException(
"Could not decode response body of type '" + contentType
+ "' with target type '" + responseType.toString() + "'"));
}
return (Flux<T>) converter.get().read(responseType, response);
}
protected static Optional<HttpMessageConverter<?>> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType type,
MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canRead(type, mediaType))
.findFirst();
}
}

View File

@@ -0,0 +1,229 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.codec.xml.Jaxb2Decoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.util.ClassUtils;
import reactor.core.publisher.Mono;
/**
* Reactive Web client supporting the HTTP/1.1 protocol
*
* <p>Here is a simple example of a GET request:
*
* <pre class="code">
* // should be shared between HTTP calls
* WebClient client = new WebClient(new ReactorHttpClient());
*
* Mono&lt;String&gt; result = client
* .perform(ClientWebRequestBuilders.get("http://example.org/resource")
* .accept(MediaType.TEXT_PLAIN))
* .extract(ResponseExtractors.body(String.class));
* </pre>
*
* <p>This Web client relies on
* <ul>
* <li>an {@link ClientHttpConnector} implementation that drives the underlying library (e.g. Reactor-Netty)</li>
* <li>a {@link ClientWebRequestBuilder} which creates a Web request with a builder API (see
* {@link ClientWebRequestBuilders})</li>
* <li>an {@link ResponseExtractor} which extracts the relevant part of the server
* response with the composition API of choice (see {@link ResponseExtractors}</li>
* </ul>
*
* @author Brian Clozel
* @see ClientWebRequestBuilders
* @see ResponseExtractors
*/
public final class WebClient {
private static final ClassLoader classLoader = WebClient.class.getClassLoader();
private static final boolean jackson2Present = ClassUtils
.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
classLoader);
private static final boolean jaxb2Present = ClassUtils
.isPresent("javax.xml.bind.Binder", classLoader);
private ClientHttpConnector clientHttpConnector;
private List<HttpMessageConverter<?>> messageConverters;
/**
* Create a {@code WebClient} instance, using the {@link ClientHttpConnector}
* implementation given as an argument to drive the underlying
* implementation.
*
* Register by default the following Encoders and Decoders:
* <ul>
* <li>{@link ByteBufferEncoder} / {@link ByteBufferDecoder}</li>
* <li>{@link StringEncoder} / {@link StringDecoder}</li>
* <li>{@link Jaxb2Encoder} / {@link Jaxb2Decoder}</li>
* <li>{@link JacksonJsonEncoder} / {@link JacksonJsonDecoder}</li>
* </ul>
*
* @param clientHttpConnector the {@code ClientHttpRequestFactory} to use
*/
public WebClient(ClientHttpConnector clientHttpConnector) {
this.clientHttpConnector = clientHttpConnector;
this.messageConverters = new ArrayList<>();
addDefaultHttpMessageConverters(this.messageConverters);
}
/**
* Adds default HTTP message converters
*/
protected final void addDefaultHttpMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder()));
converters.add(converter(new StringEncoder(), new StringDecoder()));
converters.add(new ResourceHttpMessageConverter());
if (jaxb2Present) {
converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder()));
}
if (jackson2Present) {
converters.add(converter(new JacksonJsonEncoder(), new JacksonJsonDecoder()));
}
}
private static <T> HttpMessageConverter<T> converter(Encoder<T> encoder,
Decoder<T> decoder) {
return new CodecHttpMessageConverter<>(encoder, decoder);
}
/**
* Set the list of {@link HttpMessageConverter}s to use for encoding and decoding HTTP
* messages
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
}
/**
* Perform the actual HTTP request/response exchange
*
* <p>
* Requesting from the exposed {@code Flux} will result in:
* <ul>
* <li>building the actual HTTP request using the provided {@code ClientWebRequestBuilder}</li>
* <li>encoding the HTTP request body with the configured {@code HttpMessageConverter}s</li>
* <li>returning the response with a publisher of the body</li>
* </ul>
*/
public WebResponseActions perform(ClientWebRequestBuilder builder) {
ClientWebRequest clientWebRequest = builder.build();
final Mono<ClientHttpResponse> clientResponse = this.clientHttpConnector
.connect(clientWebRequest.getMethod(), clientWebRequest.getUrl(),
new DefaultRequestCallback(clientWebRequest))
.log("org.springframework.web.client.reactive", Level.FINE);
return new WebResponseActions() {
@Override
public void doWithStatus(Consumer<HttpStatus> consumer) {
clientResponse.doOnNext(clientHttpResponse ->
consumer.accept(clientHttpResponse.getStatusCode()));
}
@Override
public <T> T extract(ResponseExtractor<T> extractor) {
return extractor.extract(clientResponse, messageConverters);
}
};
}
protected class DefaultRequestCallback implements Function<ClientHttpRequest, Mono<Void>> {
private final ClientWebRequest clientWebRequest;
public DefaultRequestCallback(ClientWebRequest clientWebRequest) {
this.clientWebRequest = clientWebRequest;
}
@Override
public Mono<Void> apply(ClientHttpRequest clientHttpRequest) {
clientHttpRequest.getHeaders().putAll(this.clientWebRequest.getHttpHeaders());
if (clientHttpRequest.getHeaders().getAccept().isEmpty()) {
clientHttpRequest.getHeaders().setAccept(
Collections.singletonList(MediaType.ALL));
}
clientWebRequest.getCookies().values()
.stream().flatMap(cookies -> cookies.stream())
.forEach(cookie -> clientHttpRequest.getCookies().add(cookie.getName(), cookie));
if (this.clientWebRequest.getBody() != null) {
return writeRequestBody(this.clientWebRequest.getBody(),
this.clientWebRequest.getElementType(), clientHttpRequest, messageConverters);
}
else {
return clientHttpRequest.setComplete();
}
}
@SuppressWarnings("unchecked")
protected Mono<Void> writeRequestBody(Publisher<?> content,
ResolvableType requestType, ClientHttpRequest request,
List<HttpMessageConverter<?>> messageConverters) {
MediaType contentType = request.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> converter = resolveConverter(messageConverters, requestType, contentType);
if (!converter.isPresent()) {
return Mono.error(new IllegalStateException(
"Could not encode request body of type '" + contentType
+ "' with target type '" + requestType.toString() + "'"));
}
return converter.get().write((Publisher) content, requestType, contentType, request);
}
protected Optional<HttpMessageConverter<?>> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType type,
MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canWrite(type, mediaType)).findFirst();
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import org.springframework.core.NestedRuntimeException;
/**
* Base class for exceptions thrown by {@link WebClient} whenever
* it encounters client-side errors.
*
* @author Brian Clozel
*/
@SuppressWarnings("serial")
public class WebClientException extends NestedRuntimeException {
public WebClientException(String msg) {
super(msg);
}
public WebClientException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive;
import java.util.function.Consumer;
import org.springframework.http.HttpStatus;
/**
* Allows applying actions, such as extractors, on the result of an executed
* {@link WebClient} request.
*
* @author Brian Clozel
*/
public interface WebResponseActions {
/**
* Apply synchronous operations once the HTTP response status
* has been received.
*/
void doWithStatus(Consumer<HttpStatus> consumer);
/**
* Perform an extraction of the response body into a higher level representation.
*
* <pre class="code">
* static imports: HttpRequestBuilders.*, HttpResponseExtractors.*
*
* webClient
* .perform(get(baseUrl.toString()).accept(MediaType.TEXT_PLAIN))
* .extract(response(String.class));
* </pre>
*/
<T> T extract(ResponseExtractor<T> extractor);
}

View File

@@ -0,0 +1,170 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive.support;
import java.net.URI;
import reactor.core.converter.RxJava1ObservableConverter;
import reactor.core.converter.RxJava1SingleConverter;
import reactor.core.publisher.Mono;
import rx.Observable;
import rx.Single;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.reactive.ClientWebRequest;
import org.springframework.web.client.reactive.ClientWebRequestBuilder;
import org.springframework.web.client.reactive.ClientWebRequestPostProcessor;
import org.springframework.web.client.reactive.DefaultClientWebRequestBuilder;
/**
* Builds a {@link ClientHttpRequest} using a {@code Observable}
* or {@code Single} as request body.
*
* <p>See static factory methods in {@link RxJava1ClientWebRequestBuilders}
*
* @author Brian Clozel
* @see RxJava1ClientWebRequestBuilders
*/
public class RxJava1ClientWebRequestBuilder implements ClientWebRequestBuilder {
private final DefaultClientWebRequestBuilder delegate;
public RxJava1ClientWebRequestBuilder(HttpMethod httpMethod, String urlTemplate,
Object... urlVariables) throws RestClientException {
this.delegate = new DefaultClientWebRequestBuilder(httpMethod, urlTemplate, urlVariables);
}
public RxJava1ClientWebRequestBuilder(HttpMethod httpMethod, URI url) {
this.delegate = new DefaultClientWebRequestBuilder(httpMethod, url);
}
/**
* Add an HTTP request header
*/
public RxJava1ClientWebRequestBuilder header(String name, String... values) {
this.delegate.header(name, values);
return this;
}
/**
* Add all provided HTTP request headers
*/
public RxJava1ClientWebRequestBuilder headers(HttpHeaders httpHeaders) {
this.delegate.headers(httpHeaders);
return this;
}
/**
* Set the Content-Type request header to the given {@link MediaType}
*/
public RxJava1ClientWebRequestBuilder contentType(MediaType contentType) {
this.delegate.contentType(contentType);
return this;
}
/**
* Set the Content-Type request header to the given media type
*/
public RxJava1ClientWebRequestBuilder contentType(String contentType) {
this.delegate.contentType(contentType);
return this;
}
/**
* Set the Accept request header to the given {@link MediaType}s
*/
public RxJava1ClientWebRequestBuilder accept(MediaType... mediaTypes) {
this.delegate.accept(mediaTypes);
return this;
}
/**
* Set the Accept request header to the given media types
*/
public RxJava1ClientWebRequestBuilder accept(String... mediaTypes) {
this.delegate.accept(mediaTypes);
return this;
}
/**
* Add a Cookie to the HTTP request
*/
public RxJava1ClientWebRequestBuilder cookie(String name, String value) {
this.delegate.cookie(name, value);
return this;
}
/**
* Add a Cookie to the HTTP request
*/
public RxJava1ClientWebRequestBuilder cookie(HttpCookie cookie) {
this.delegate.cookie(cookie);
return this;
}
/**
* Allows performing more complex operations with a strategy. For example, a
* {@link ClientWebRequestPostProcessor} implementation might accept the arguments of username
* and password and set an HTTP Basic authentication header.
*
* @param postProcessor the {@link ClientWebRequestPostProcessor} to use. Cannot be null.
*
* @return this instance for further modifications.
*/
public RxJava1ClientWebRequestBuilder apply(ClientWebRequestPostProcessor postProcessor) {
this.delegate.apply(postProcessor);
return this;
}
/**
* Use the given object as the request body
*/
public RxJava1ClientWebRequestBuilder body(Object content) {
this.delegate.body(Mono.just(content), ResolvableType.forInstance(content));
return this;
}
/**
* Use the given {@link Single} as the request body and use its {@link ResolvableType}
* as type information for the element published by this reactive stream
*/
public RxJava1ClientWebRequestBuilder body(Single<?> content, ResolvableType elementType) {
this.delegate.body(RxJava1SingleConverter.toPublisher(content), elementType);
return this;
}
/**
* Use the given {@link Observable} as the request body and use its {@link ResolvableType}
* as type information for the elements published by this reactive stream
*/
public RxJava1ClientWebRequestBuilder body(Observable<?> content, ResolvableType elementType) {
this.delegate.body(RxJava1ObservableConverter.toPublisher(content), elementType);
return this;
}
@Override
public ClientWebRequest build() {
return this.delegate.build();
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive.support;
import org.springframework.http.HttpMethod;
/**
* Static factory methods for {@link RxJava1ClientWebRequestBuilder ClientWebRequestBuilders}
* using the {@link rx.Observable} and {@link rx.Single} API.
*
* @author Brian Clozel
*/
public abstract class RxJava1ClientWebRequestBuilders {
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a GET request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder get(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a POST request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder post(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a PUT request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder put(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a PATCH request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder patch(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a DELETE request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder delete(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for an OPTIONS request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder options(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a HEAD request.
*
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder head(String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables);
}
/**
* Create a {@link RxJava1ClientWebRequestBuilder} for a request with the given HTTP method.
*
* @param httpMethod the HTTP method
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param urlVariables zero or more URL variables
*/
public static RxJava1ClientWebRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) {
return new RxJava1ClientWebRequestBuilder(httpMethod, urlTemplate, urlVariables);
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.client.reactive.support;
import java.util.List;
import java.util.Optional;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.web.client.reactive.ResponseExtractor;
import reactor.core.converter.RxJava1ObservableConverter;
import reactor.core.converter.RxJava1SingleConverter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Observable;
import rx.Single;
/**
* Static factory methods for {@link ResponseExtractor}
* based on the {@link Observable} and {@link Single} API.
*
* @author Brian Clozel
*/
public class RxJava1ResponseExtractors {
/**
* Extract the response body and decode it, returning it as a {@code Single<T>}
*/
@SuppressWarnings("unchecked")
public static <T> ResponseExtractor<Single<T>> body(Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return (clientResponse, messageConverters) -> (Single<T>) RxJava1SingleConverter
.fromPublisher(clientResponse
.flatMap(resp -> decodeResponseBody(resp, resolvableType, messageConverters)).next());
}
/**
* Extract the response body and decode it, returning it as an {@code Observable<T>}
*/
public static <T> ResponseExtractor<Observable<T>> bodyStream(Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return (clientResponse, messageConverters) -> RxJava1ObservableConverter
.fromPublisher(clientResponse
.flatMap(resp -> decodeResponseBody(resp, resolvableType, messageConverters)));
}
/**
* Extract the full response body as a {@code ResponseEntity}
* with its body decoded as a single type {@code T}
*/
@SuppressWarnings("unchecked")
public static <T> ResponseExtractor<Single<ResponseEntity<T>>> response(Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return (clientResponse, messageConverters) ->
RxJava1SingleConverter.fromPublisher(clientResponse
.then(response ->
Mono.when(
decodeResponseBody(response, resolvableType, messageConverters).next(),
Mono.just(response.getHeaders()),
Mono.just(response.getStatusCode())))
.map(tuple ->
new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3())));
}
/**
* Extract the full response body as a {@code ResponseEntity}
* with its body decoded as an {@code Observable<T>}
*/
public static <T> ResponseExtractor<Single<ResponseEntity<Observable<T>>>> responseStream(Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return (clientResponse, messageConverters) -> RxJava1SingleConverter.fromPublisher(clientResponse
.map(response -> new ResponseEntity<>(
RxJava1ObservableConverter
.fromPublisher(decodeResponseBody(response, resolvableType, messageConverters)),
response.getHeaders(),
response.getStatusCode())));
}
/**
* Extract the response headers as an {@code HttpHeaders} instance
*/
public static ResponseExtractor<Single<HttpHeaders>> headers() {
return (clientResponse, messageConverters) -> RxJava1SingleConverter
.fromPublisher(clientResponse.map(resp -> resp.getHeaders()));
}
@SuppressWarnings("unchecked")
protected static <T> Flux<T> decodeResponseBody(ClientHttpResponse response, ResolvableType responseType,
List<HttpMessageConverter<?>> messageConverters) {
MediaType contentType = response.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> converter = resolveConverter(messageConverters, responseType, contentType);
if (!converter.isPresent()) {
return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType +
"' with target type '" + responseType.toString() + "'"));
}
return (Flux<T>) converter.get().read(responseType, response);
}
protected static Optional<HttpMessageConverter<?>> resolveConverter(List<HttpMessageConverter<?>> messageConverters,
ResolvableType type, MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canRead(type, mediaType)).findFirst();
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Collections;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
/**
* Exception for errors that fit response status 415 (unsupported media type).
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class MediaTypeNotSupportedStatusException extends ResponseStatusException {
private final List<MediaType> supportedMediaTypes;
/**
* Constructor for when the Content-Type is invalid.
*/
public MediaTypeNotSupportedStatusException(String reason) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason);
this.supportedMediaTypes = Collections.emptyList();
}
/**
* Constructor for when the Content-Type is not supported.
*/
public MediaTypeNotSupportedStatusException(List<MediaType> supportedMediaTypes) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported media type", null);
this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes);
}
/**
* Return the list of supported content types in cases when the Accept
* header is parsed but not supported, or an empty list otherwise.
*/
public List<MediaType> getSupportedMediaTypes() {
return this.supportedMediaTypes;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
/**
* Exception for errors that fit response status 405 (method not allowed).
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class MethodNotAllowedException extends ResponseStatusException {
private String method;
private Set<String> supportedMethods;
public MethodNotAllowedException(String method, Collection<String> supportedMethods) {
super(HttpStatus.METHOD_NOT_ALLOWED, "Request method '" + method + "' not supported");
Assert.notNull(method, "'method' is required");
this.method = method;
this.supportedMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods));
}
/**
* Return the HTTP method for the failed request.
*/
public String getHttpMethod() {
return this.method;
}
/**
* Return the list of supported HTTP methods.
*/
public Set<String> getSupportedMethods() {
return supportedMethods;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Collections;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
/**
* Exception for errors that fit response status 405 (not acceptable).
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class NotAcceptableStatusException extends ResponseStatusException {
private final List<MediaType> supportedMediaTypes;
/**
* Constructor for when the requested Content-Type is invalid.
*/
public NotAcceptableStatusException(String reason) {
super(HttpStatus.NOT_ACCEPTABLE, reason);
this.supportedMediaTypes = Collections.emptyList();
}
/**
* Constructor for when requested Content-Type is not supported.
*/
public NotAcceptableStatusException(List<MediaType> supportedMediaTypes) {
super(HttpStatus.NOT_ACCEPTABLE, "Could not find acceptable representation", null);
this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes);
}
/**
* Return the list of supported content types in cases when the Accept
* header is parsed but not supported, or an empty list otherwise.
*/
public List<MediaType> getSupportedMediaTypes() {
return this.supportedMediaTypes;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
/**
* Base class for exceptions associated with specific HTTP response status codes.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class ResponseStatusException extends NestedRuntimeException {
private final HttpStatus status;
private final String reason;
/**
* Constructor with a response code and a reason to add to the exception
* message as explanation.
*/
public ResponseStatusException(HttpStatus status, String reason) {
this(status, reason, null);
}
/**
* Constructor with a nested exception.
*/
public ResponseStatusException(HttpStatus status, String reason, Throwable cause) {
super("Request failure [status: " + status + ", reason: \"" + reason + "\"]", cause);
Assert.notNull(status, "'status' is required");
Assert.notNull(reason, "'reason' is required");
this.status = status;
this.reason = reason;
}
/**
* The HTTP status that fits the exception.
*/
public HttpStatus getStatus() {
return this.status;
}
/**
* The reason explaining the exception.
*/
public String getReason() {
return this.reason;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Optional;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
/**
* Exception for errors that fit response status 500 (bad request) for use in
* Spring Web applications. The exception provides additional fields (e.g.
* an optional {@link MethodParameter} if related to the error).
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class ServerErrorException extends ResponseStatusException {
private final MethodParameter parameter;
/**
* Constructor with an explanation only.
*/
public ServerErrorException(String reason) {
this(reason, null);
}
/**
* Constructor for a 500 error linked to a specific {@code MethodParameter}.
*/
public ServerErrorException(String reason, MethodParameter parameter) {
this(reason, parameter, null);
}
/**
* Constructor for a 500 error with a root cause.
*/
public ServerErrorException(String reason, MethodParameter parameter, Throwable cause) {
super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause);
this.parameter = parameter;
}
/**
* Return the {@code MethodParameter} associated with this error, if any.
*/
public Optional<MethodParameter> getMethodParameter() {
return Optional.ofNullable(this.parameter);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Map;
import java.util.Optional;
import reactor.core.publisher.Mono;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* Contract for an HTTP request-response interaction. Provides access to the HTTP
* request and response and also exposes additional server-side processing
* related properties and features such as request attributes.
*
* @author Rossen Stoyanchev
*/
public interface ServerWebExchange {
/**
* Return the current HTTP request.
*/
ServerHttpRequest getRequest();
/**
* Return the current HTTP response.
*/
ServerHttpResponse getResponse();
/**
* Return a mutable map of request attributes for the current exchange.
*/
Map<String, Object> getAttributes();
/**
* Return the request attribute value if present.
* @param name the attribute name
* @param <T> the attribute type
* @return the attribute value
*/
<T> Optional<T> getAttribute(String name);
/**
* Return the web session for the current request. Always guaranteed to
* return an instance either matching to the session id requested by the
* client, or with a new session id either because the client did not
* specify one or because the underlying session had expired. Use of this
* method does not automatically create a session. See {@link WebSession}
* for more details.
*/
Mono<WebSession> getSession();
}

View File

@@ -0,0 +1,66 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Optional;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
/**
* Exception for errors that fit response status 400 (bad request) for use in
* Spring Web applications. The exception provides additional fields (e.g.
* an optional {@link MethodParameter} if related to the error).
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class ServerWebInputException extends ResponseStatusException {
private final MethodParameter parameter;
/**
* Constructor with an explanation only.
*/
public ServerWebInputException(String reason) {
this(reason, null);
}
/**
* Constructor for a 400 error linked to a specific {@code MethodParameter}.
*/
public ServerWebInputException(String reason, MethodParameter parameter) {
this(reason, parameter, null);
}
/**
* Constructor for a 400 error with a root cause.
*/
public ServerWebInputException(String reason, MethodParameter parameter, Throwable cause) {
super(HttpStatus.BAD_REQUEST, reason, cause);
this.parameter = parameter;
}
/**
* Return the {@code MethodParameter} associated with this error, if any.
*/
public Optional<MethodParameter> getMethodParameter() {
return Optional.ofNullable(this.parameter);
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
/**
* Exception for errors that fit response status 416 (unsupported media type).
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("serial")
public class UnsupportedMediaTypeStatusException extends ResponseStatusException {
private final MediaType contentType;
private final List<MediaType> supportedMediaTypes;
/**
* Constructor for when the specified Content-Type is invalid.
*/
public UnsupportedMediaTypeStatusException(String reason) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason);
this.contentType = null;
this.supportedMediaTypes = Collections.emptyList();
}
/**
* Constructor for when the Content-Type can be parsed but is not supported.
*/
public UnsupportedMediaTypeStatusException(MediaType contentType, List<MediaType> supportedMediaTypes) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Content type '" + contentType + "' not supported");
this.contentType = contentType;
this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes);
}
/**
* Return the request Content-Type header if it was parsed successfully.
*/
public Optional<MediaType> getContentType() {
return Optional.ofNullable(this.contentType);
}
/**
* Return the list of supported content types in cases when the Content-Type
* header is parsed but not supported, or an empty list otherwise.
*/
public List<MediaType> getSupportedMediaTypes() {
return this.supportedMediaTypes;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import reactor.core.publisher.Mono;
/**
* Contract for handling exceptions during web server exchange processing.
*
* @author Rossen Stoyanchev
*/
public interface WebExceptionHandler {
/**
* Handle the given exception. A completion signal through the return value
* indicates error handling is complete while an error signal indicates the
* exception is still not handled.
*
* @param exchange the current exchange
* @param ex the exception to handle
* @return {@code Mono<Void>} to indicate when exception handling is complete
*/
Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import reactor.core.publisher.Mono;
/**
* Contract for interception-style, chained processing of Web requests that may
* be used to implement cross-cutting, application-agnostic requirements such
* as security, timeouts, and others.
*
* @author Rossen Stoyanchev
*/
public interface WebFilter {
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link WebFilterChain}.
*
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import reactor.core.publisher.Mono;
/**
* Contract to allow a {@link WebFilter} to delegate to the next in the chain.
*
* @author Rossen Stoyanchev
*/
public interface WebFilterChain {
/**
* Delegate to the next {@code WebFilter} in the chain.
*
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
Mono<Void> filter(ServerWebExchange exchange);
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import reactor.core.publisher.Mono;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Contract to handle a web request.
*
* <p>Use {@link HttpWebHandlerAdapter} to adapt a {@code WebHandler} to an
* {@link org.springframework.http.server.reactive.HttpHandler HttpHandler}.
* The {@link WebHttpHandlerBuilder} provides a convenient way to do that while
* also optionally configuring one or more filters and/or exception handlers.
*
* @author Rossen Stoyanchev
*/
public interface WebHandler {
/**
* Handle the web server exchange.
*
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
Mono<Void> handle(ServerWebExchange exchange);
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import reactor.core.publisher.Mono;
/**
* Main contract for using a server-side session that provides access to session
* attributes across HTTP requests.
*
* <p>The creation of a {@code WebSession} instance does not automatically start
* a session thus causing the session id to be sent to the client (typically via
* a cookie). A session starts implicitly when session attributes are added.
* A session may also be created explicitly via {@link #start()}.
*
* @author Rossen Stoyanchev
*/
public interface WebSession {
/**
* Return a unique session identifier.
*/
String getId();
/**
* Return a map that holds session attributes.
*/
Map<String, Object> getAttributes();
/**
* Return the attribute value if present.
* @param name the attribute name
* @param <T> the attribute type
* @return the attribute value
*/
<T> Optional<T> getAttribute(String name);
/**
* Force the creation of a session causing the session id to be sent when
* {@link #save()} is called.
*/
void start();
/**
* Whether a session with the client has been started explicitly via
* {@link #start()} or implicitly by adding session attributes.
* If "false" then the session id is not sent to the client and the
* {@link #save()} method is essentially a no-op.
*/
boolean isStarted();
/**
* Save the session persisting attributes (e.g. if stored remotely) and also
* sending the session id to the client if the session is new.
* <p>Note that a session must be started explicitly via {@link #start()} or
* implicitly by adding attributes or otherwise this method has no effect.
* @return {@code Mono} to indicate completion with success or error
* <p>Typically this method should be automatically invoked just before the
* response is committed so applications don't have to by default.
*/
Mono<Void> save();
/**
* Return {@code true} if the session expired after {@link #getMaxIdleTime()
* maxIdleTime} elapsed.
* <p>Typically expiration checks should be automatically made when a session
* is accessed, a new {@code WebSession} instance created if necessary, at
* the start of request processing so that applications don't have to worry
* about expired session by default.
*/
boolean isExpired();
/**
* Return the time when the session was created.
*/
Instant getCreationTime();
/**
* Return the last time of session access as a result of user activity such
* as an HTTP request. Together with {@link #getMaxIdleTime()
* maxIdleTimeInSeconds} this helps to determine when a session is
* {@link #isExpired() expired}.
*/
Instant getLastAccessTime();
/**
* Configure the max amount of time that may elapse after the
* {@link #getLastAccessTime() lastAccessTime} before a session is considered
* expired. A negative value indicates the session should not expire.
*/
void setMaxIdleTime(Duration maxIdleTime);
/**
* Return the maximum time after the {@link #getLastAccessTime()
* lastAccessTime} before a session expires. A negative time indicates the
* session doesn't expire.
*/
Duration getMaxIdleTime();
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.adapter;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionManager;
/**
* Default implementation of {@link ServerWebExchange}.
*
* @author Rossen Stoyanchev
*/
public class DefaultServerWebExchange implements ServerWebExchange {
private final ServerHttpRequest request;
private final ServerHttpResponse response;
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
private final Mono<WebSession> sessionMono;
public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
WebSessionManager sessionManager) {
Assert.notNull(request, "'request' is required.");
Assert.notNull(response, "'response' is required.");
Assert.notNull(response, "'sessionManager' is required.");
this.request = request;
this.response = response;
this.sessionMono = sessionManager.getSession(this).cache();
}
@Override
public ServerHttpRequest getRequest() {
return this.request;
}
@Override
public ServerHttpResponse getResponse() {
return this.response;
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
@Override @SuppressWarnings("unchecked")
public <T> Optional<T> getAttribute(String name) {
return Optional.ofNullable((T) this.attributes.get(name));
}
@Override
public Mono<WebSession> getSession() {
return this.sessionMono;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.adapter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.WebHandlerDecorator;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
/**
* Default adapter of {@link WebHandler} to the {@link HttpHandler} contract.
*
* <p>By default creates and configures a {@link DefaultServerWebExchange} and
* then invokes the target {@code WebHandler}.
*
* @author Rossen Stoyanchev
*/
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler {
private static Log logger = LogFactory.getLog(HttpWebHandlerAdapter.class);
private WebSessionManager sessionManager = new DefaultWebSessionManager();
public HttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
}
/**
* Configure a custom {@link WebSessionManager} to use for managing web
* sessions. The provided instance is set on each created
* {@link DefaultServerWebExchange}.
* <p>By default this is set to {@link DefaultWebSessionManager}.
* @param sessionManager the session manager to use
*/
public void setSessionManager(WebSessionManager sessionManager) {
Assert.notNull(sessionManager, "'sessionManager' must not be null.");
this.sessionManager = sessionManager;
}
/**
* Return the configured {@link WebSessionManager}.
*/
public WebSessionManager getSessionManager() {
return this.sessionManager;
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
ServerWebExchange exchange = createExchange(request, response);
return getDelegate().handle(exchange)
.otherwise(ex -> {
if (logger.isDebugEnabled()) {
logger.debug("Could not complete request", ex);
}
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return Mono.empty();
})
.then(response::setComplete);
}
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
return new DefaultServerWebExchange(request, response, this.sessionManager);
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.adapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
import org.springframework.web.server.handler.FilteringWebHandler;
import org.springframework.web.server.session.WebSessionManager;
/**
* Builder for an {@link HttpHandler} that adapts to a target {@link WebHandler}
* along with a chain of {@link WebFilter}s and a set of
* {@link WebExceptionHandler}s.
*
* <p>Example usage:
* <pre>
* WebFilter filter = ... ;
* WebHandler webHandler = ... ;
* WebExceptionHandler exceptionHandler = ...;
*
* HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(webHandler)
* .filters(filter)
* .exceptionHandlers(exceptionHandler)
* .build();
* </pre>
*
* @author Rossen Stoyanchev
*/
public class WebHttpHandlerBuilder {
private final WebHandler targetHandler;
private final List<WebFilter> filters = new ArrayList<>();
private final List<WebExceptionHandler> exceptionHandlers = new ArrayList<>();
private WebSessionManager sessionManager;
/**
* Private constructor.
* See factory method {@link #webHandler(WebHandler)}.
*/
private WebHttpHandlerBuilder(WebHandler targetHandler) {
Assert.notNull(targetHandler, "'targetHandler' must not be null");
this.targetHandler = targetHandler;
}
/**
* Factory method to create a new builder instance.
* @param webHandler the target handler for the request
*/
public static WebHttpHandlerBuilder webHandler(WebHandler webHandler) {
return new WebHttpHandlerBuilder(webHandler);
}
/**
* Add the given filters to use for processing requests.
* @param filters the filters to add
*/
public WebHttpHandlerBuilder filters(WebFilter... filters) {
if (!ObjectUtils.isEmpty(filters)) {
this.filters.addAll(Arrays.asList(filters));
}
return this;
}
/**
* Add the given exception handler to apply at the end of request processing.
* @param exceptionHandlers the exception handlers
*/
public WebHttpHandlerBuilder exceptionHandlers(WebExceptionHandler... exceptionHandlers) {
if (!ObjectUtils.isEmpty(exceptionHandlers)) {
this.exceptionHandlers.addAll(Arrays.asList(exceptionHandlers));
}
return this;
}
/**
* Configure the {@link WebSessionManager} to set on the
* {@link ServerWebExchange WebServerExchange}
* created for each HTTP request.
* @param sessionManager the session manager
* @see HttpWebHandlerAdapter#setSessionManager(WebSessionManager)
*/
public WebHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) {
this.sessionManager = sessionManager;
return this;
}
/**
* Build the {@link HttpHandler}.
*/
public HttpHandler build() {
WebHandler webHandler = this.targetHandler;
if (!this.filters.isEmpty()) {
WebFilter[] array = new WebFilter[this.filters.size()];
webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array));
}
if (!this.exceptionHandlers.isEmpty()) {
WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()];
webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array));
}
HttpWebHandlerAdapter httpHandler = new HttpWebHandlerAdapter(webHandler);
if (this.sessionManager != null) {
httpHandler.setSessionManager(this.sessionManager);
}
return httpHandler;
}
}

View File

@@ -0,0 +1,6 @@
/**
* Implementation support to adapt
* {@link org.springframework.web.server Spring web server} to the underlying
* {@link org.springframework.http.server.reactive HTTP server} layer.
*/
package org.springframework.web.server.adapter;

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.handler;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.ServerWebExchange;
/**
* WebHandler that can invoke a target {@link WebHandler} and then apply
* exception handling with one or more {@link WebExceptionHandler} instances.
*
* @author Rossen Stoyanchev
*/
public class ExceptionHandlingWebHandler extends WebHandlerDecorator {
private static Log logger = LogFactory.getLog(ExceptionHandlingWebHandler.class);
private final List<WebExceptionHandler> exceptionHandlers;
public ExceptionHandlingWebHandler(WebHandler delegate, WebExceptionHandler... exceptionHandlers) {
super(delegate);
this.exceptionHandlers = initList(exceptionHandlers);
}
private static List<WebExceptionHandler> initList(WebExceptionHandler[] list) {
return (list != null ? Collections.unmodifiableList(Arrays.asList(list)): Collections.emptyList());
}
/**
* Return a read-only list of the configured exception handlers.
*/
public List<WebExceptionHandler> getExceptionHandlers() {
return this.exceptionHandlers;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Mono<Void> mono;
try {
mono = getDelegate().handle(exchange);
}
catch (Throwable ex) {
mono = Mono.error(ex);
}
for (WebExceptionHandler exceptionHandler : this.exceptionHandlers) {
mono = mono.otherwise(ex -> exceptionHandler.handle(exchange, ex));
}
return mono.otherwise(ex -> handleUnresolvedException(exchange, ex));
}
private Mono<? extends Void> handleUnresolvedException(ServerWebExchange exchange, Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not complete request", ex);
}
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return Mono.empty();
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.handler;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.ServerWebExchange;
/**
* WebHandler that delegates to a chain of {@link WebFilter} instances and then
* to the target {@link WebHandler}.
*
* @author Rossen Stoyanchev
*/
public class FilteringWebHandler extends WebHandlerDecorator {
private final List<WebFilter> filters;
public FilteringWebHandler(WebHandler targetHandler, WebFilter... filters) {
super(targetHandler);
this.filters = initList(filters);
}
private static List<WebFilter> initList(WebFilter[] list) {
return (list != null ? Collections.unmodifiableList(Arrays.asList(list)): Collections.emptyList());
}
/**
* Return a read-only list of the configured filters.
*/
public List<WebFilter> getFilters() {
return this.filters;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return new DefaultWebFilterChain().filter(exchange);
}
private class DefaultWebFilterChain implements WebFilterChain {
private int index;
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
if (this.index < filters.size()) {
WebFilter filter = filters.get(this.index++);
return filter.filter(exchange, this);
}
else {
return getDelegate().handle(exchange);
}
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.handler;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.ServerWebExchange;
/**
* {@link WebHandler} that decorates and delegates to another.
*
* @author Rossen Stoyanchev
*/
public class WebHandlerDecorator implements WebHandler {
private final WebHandler delegate;
public WebHandlerDecorator(WebHandler delegate) {
Assert.notNull(delegate, "'delegate' must not be null");
this.delegate = delegate;
}
public WebHandler getDelegate() {
return this.delegate;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return this.delegate.handle(exchange);
}
@Override
public String toString() {
return getClass().getSimpleName() + " [delegate=" + this.delegate + "]";
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides WebHandler implementations.
*/
package org.springframework.web.server.handler;

View File

@@ -0,0 +1,4 @@
/**
* Foundational Spring web server support.
*/
package org.springframework.web.server;

View File

@@ -0,0 +1,45 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import java.time.Instant;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.web.server.WebSession;
/**
* Extend {@link WebSession} with management operations meant for internal use
* for example by implementations of {@link WebSessionManager}.
*
* @author Rossen Stoyanchev
*/
public interface ConfigurableWebSession extends WebSession {
/**
* Update the last access time for user-related session activity.
* @param time the time of access
*/
void setLastAccessTime(Instant time);
/**
* Set the operation to invoke when {@link WebSession#save()} is invoked.
* @param saveOperation the save operation
*/
void setSaveOperation(Supplier<Mono<Void>> saveOperation);
}

View File

@@ -0,0 +1,95 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpCookie;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* Cookie-based {@link WebSessionIdResolver}.
*
* @author Rossen Stoyanchev
*/
public class CookieWebSessionIdResolver implements WebSessionIdResolver {
private String cookieName = "SESSION";
private Duration cookieMaxAge = Duration.ofSeconds(-1);
/**
* Set the name of the cookie to use for the session id.
* <p>By default set to "SESSION".
* @param cookieName the cookie name
*/
public void setCookieName(String cookieName) {
Assert.hasText(cookieName, "'cookieName' must not be empty.");
this.cookieName = cookieName;
}
/**
* Return the configured cookie name.
*/
public String getCookieName() {
return this.cookieName;
}
/**
* Set the value for the "Max-Age" attribute of the cookie that holds the
* session id. For the range of values see {@link ResponseCookie#getMaxAge()}.
* <p>By default set to -1.
* @param maxAge the maxAge duration value
*/
public void setCookieMaxAge(Duration maxAge) {
this.cookieMaxAge = maxAge;
}
/**
* Return the configured "Max-Age" attribute value for the session cookie.
*/
public Duration getCookieMaxAge() {
return this.cookieMaxAge;
}
@Override
public List<String> resolveSessionIds(ServerWebExchange exchange) {
MultiValueMap<String, HttpCookie> cookieMap = exchange.getRequest().getCookies();
List<HttpCookie> cookies = cookieMap.get(getCookieName());
if (cookies == null) {
return Collections.emptyList();
}
return cookies.stream().map(HttpCookie::getValue).collect(Collectors.toList());
}
@Override
public void setSessionId(ServerWebExchange exchange, String id) {
Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0));
ResponseCookie cookie = ResponseCookie.from(getCookieName(), id).maxAge(maxAge).build();
MultiValueMap<String, ResponseCookie> cookieMap = exchange.getResponse().getCookies();
cookieMap.set(getCookieName(), cookie);
}
}

View File

@@ -0,0 +1,178 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import java.io.Serializable;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
/**
* @author Rossen Stoyanchev
*/
public class DefaultWebSession implements ConfigurableWebSession, Serializable {
private static final long serialVersionUID = -3567697426432961630L;
private final String id;
private final Map<String, Object> attributes;
private final Clock clock;
private final Instant creationTime;
private volatile Instant lastAccessTime;
private volatile Duration maxIdleTime;
private AtomicReference<State> state = new AtomicReference<>();
private volatile transient Supplier<Mono<Void>> saveOperation;
/**
* Constructor to create a new session.
* @param id the session id
* @param clock for access to current time
*/
public DefaultWebSession(String id, Clock clock) {
Assert.notNull(id, "'id' is required.");
Assert.notNull(clock, "'clock' is required.");
this.id = id;
this.clock = clock;
this.attributes = new ConcurrentHashMap<>();
this.creationTime = Instant.now(clock);
this.lastAccessTime = this.creationTime;
this.maxIdleTime = Duration.ofMinutes(30);
this.state.set(State.NEW);
}
/**
* Constructor to load existing session.
* @param id the session id
* @param attributes the attributes of the session
* @param clock for access to current time
* @param creationTime the creation time
* @param lastAccessTime the last access time
* @param maxIdleTime the configured maximum session idle time
*/
public DefaultWebSession(String id, Map<String, Object> attributes, Clock clock,
Instant creationTime, Instant lastAccessTime, Duration maxIdleTime) {
Assert.notNull(id, "'id' is required.");
Assert.notNull(clock, "'clock' is required.");
this.id = id;
this.attributes = new ConcurrentHashMap<>(attributes);
this.clock = clock;
this.creationTime = creationTime;
this.lastAccessTime = lastAccessTime;
this.maxIdleTime = maxIdleTime;
this.state.set(State.STARTED);
}
@Override
public String getId() {
return this.id;
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
@Override @SuppressWarnings("unchecked")
public <T> Optional<T> getAttribute(String name) {
return Optional.ofNullable((T) this.attributes.get(name));
}
@Override
public Instant getCreationTime() {
return this.creationTime;
}
@Override
public void setLastAccessTime(Instant lastAccessTime) {
this.lastAccessTime = lastAccessTime;
}
@Override
public Instant getLastAccessTime() {
return this.lastAccessTime;
}
/**
* <p>By default this is set to 30 minutes.
* @param maxIdleTime the max idle time
*/
@Override
public void setMaxIdleTime(Duration maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
@Override
public Duration getMaxIdleTime() {
return this.maxIdleTime;
}
@Override
public void setSaveOperation(Supplier<Mono<Void>> saveOperation) {
Assert.notNull(saveOperation, "'saveOperation' is required.");
this.saveOperation = saveOperation;
}
protected Supplier<Mono<Void>> getSaveOperation() {
return this.saveOperation;
}
@Override
public void start() {
this.state.compareAndSet(State.NEW, State.STARTED);
}
@Override
public boolean isStarted() {
State value = this.state.get();
return (State.STARTED.equals(value) || (State.NEW.equals(value) && !getAttributes().isEmpty()));
}
@Override
public Mono<Void> save() {
return this.saveOperation.get();
}
@Override
public boolean isExpired() {
return (isStarted() && !this.maxIdleTime.isNegative() &&
Instant.now(this.clock).minus(this.maxIdleTime).isAfter(this.lastAccessTime));
}
private enum State { NEW, STARTED }
}

View File

@@ -0,0 +1,158 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
/**
* Default implementation of {@link WebSessionManager} with a cookie-based web
* session id resolution strategy and simple in-memory session persistence.
*
* @author Rossen Stoyanchev
*/
public class DefaultWebSessionManager implements WebSessionManager {
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
private WebSessionStore sessionStore = new InMemoryWebSessionStore();
private Clock clock = Clock.systemDefaultZone();
/**
* Configure the session id resolution strategy to use.
* <p>By default {@link CookieWebSessionIdResolver} is used.
* @param sessionIdResolver the resolver
*/
public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) {
Assert.notNull(sessionIdResolver, "'sessionIdResolver' is required.");
this.sessionIdResolver = sessionIdResolver;
}
/**
* Return the configured {@link WebSessionIdResolver}.
*/
public WebSessionIdResolver getSessionIdResolver() {
return this.sessionIdResolver;
}
/**
* Configure the session persistence strategy to use.
* <p>By default {@link InMemoryWebSessionStore} is used.
* @param sessionStore the persistence strategy
*/
public void setSessionStore(WebSessionStore sessionStore) {
Assert.notNull(sessionStore, "'sessionStore' is required.");
this.sessionStore = sessionStore;
}
/**
* Return the configured {@link WebSessionStore}.
*/
public WebSessionStore getSessionStore() {
return this.sessionStore;
}
/**
* Configure the {@link Clock} for access to current time. During tests you
* may use {code Clock.offset(clock, Duration.ofMinutes(-31))} to set the
* clock back for example to test changes after sessions expire.
* <p>By default {@link Clock#systemDefaultZone()} is used.
* @param clock the clock to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "'clock' is required.");
this.clock = clock;
}
/**
* Return the configured clock for access to current time.
*/
public Clock getClock() {
return this.clock;
}
@Override
public Mono<WebSession> getSession(ServerWebExchange exchange) {
return Mono.defer(() ->
Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange))
.concatMap(this.sessionStore::retrieveSession)
.next()
.then(session -> validateSession(exchange, session))
.otherwiseIfEmpty(createSession(exchange))
.map(session -> extendSession(exchange, session)));
}
protected Mono<WebSession> validateSession(ServerWebExchange exchange, WebSession session) {
if (session.isExpired()) {
this.sessionIdResolver.setSessionId(exchange, "");
return this.sessionStore.removeSession(session.getId()).cast(WebSession.class);
}
else {
return Mono.just(session);
}
}
protected Mono<WebSession> createSession(ServerWebExchange exchange) {
String sessionId = UUID.randomUUID().toString();
WebSession session = new DefaultWebSession(sessionId, getClock());
return Mono.just(session);
}
protected WebSession extendSession(ServerWebExchange exchange, WebSession session) {
if (session instanceof ConfigurableWebSession) {
ConfigurableWebSession managed = (ConfigurableWebSession) session;
managed.setSaveOperation(() -> saveSession(exchange, session));
managed.setLastAccessTime(Instant.now(getClock()));
}
exchange.getResponse().beforeCommit(session::save);
return session;
}
protected Mono<Void> saveSession(ServerWebExchange exchange, WebSession session) {
Assert.isTrue(!session.isExpired(), "Sessions are checked for expiration and have their " +
"access time updated when first accessed during request processing. " +
"However this session is expired meaning that maxIdleTime elapsed " +
"since then and before the call to session.save().");
if (!session.isStarted()) {
return Mono.empty();
}
// Force explicit start
session.start();
List<String> requestedIds = getSessionIdResolver().resolveSessionIds(exchange);
if (requestedIds.isEmpty() || !session.getId().equals(requestedIds.get(0))) {
this.sessionIdResolver.setSessionId(exchange, session.getId());
}
return this.sessionStore.storeSession(session);
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.web.server.WebSession;
/**
* Simple Map-based storage for {@link WebSession} instances.
*
* @author Rossen Stoyanchev
*/
public class InMemoryWebSessionStore implements WebSessionStore {
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
@Override
public Mono<Void> storeSession(WebSession session) {
this.sessions.put(session.getId(), session);
return Mono.empty();
}
@Override
public Mono<WebSession> retrieveSession(String id) {
return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty());
}
@Override
public Mono<Void> removeSession(String id) {
this.sessions.remove(id);
return Mono.empty();
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import java.util.List;
import org.springframework.web.server.ServerWebExchange;
/**
* Contract for session id resolution strategies. Allows for session id
* resolution through the request and for sending the session id to the
* client through the response.
*
* @author Rossen Stoyanchev
* @see CookieWebSessionIdResolver
*/
public interface WebSessionIdResolver {
/**
* Resolve the session id's associated with the request.
* @param exchange the current exchange
* @return the session id's or an empty list
*/
List<String> resolveSessionIds(ServerWebExchange exchange);
/**
* Send the given session id to the client or if the session id is "null"
* instruct the client to end the current session.
* @param exchange the current exchange
* @param sessionId the session id
*/
void setSessionId(ServerWebExchange exchange, String sessionId);
}

View File

@@ -0,0 +1,48 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import reactor.core.publisher.Mono;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
/**
* Main contract abstracting support for access to {@link WebSession} instances
* associated with HTTP requests as well as the subsequent management such as
* persistence and others.
*
* <p>The {@link DefaultWebSessionManager} implementation in turn delegates to
* {@link WebSessionIdResolver} and {@link WebSessionStore} which abstract
* underlying concerns related to the management of web sessions.
*
* @author Rossen Stoyanchev
* @see WebSessionIdResolver
* @see WebSessionStore
*/
public interface WebSessionManager {
/**
* Return the {@link WebSession} for the given exchange. Always guaranteed
* to return an instance either matching to the session id requested by the
* client, or with a new session id either because the client did not
* specify one or because the underlying session had expired.
* @param exchange the current exchange
* @return {@code Mono} for async access to the session
*/
Mono<WebSession> getSession(ServerWebExchange exchange);
}

View File

@@ -0,0 +1,51 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.server.session;
import reactor.core.publisher.Mono;
import org.springframework.web.server.WebSession;
/**
* Strategy for {@link WebSession} persistence.
*
* @author Rossen Stoyanchev
* @since 4.3
*/
public interface WebSessionStore {
/**
* Store the given session.
* @param session the session to store
* @return {@code Mono} for completion notification
*/
Mono<Void> storeSession(WebSession session);
/**
* Load the session for the given session id.
* @param sessionId the session to load
* @return {@code Mono} for async access to the loaded session
*/
Mono<WebSession> retrieveSession(String sessionId);
/**
* Remove the session with the given id.
* @param sessionId the session to remove
* @return {@code Mono} for completion notification
*/
Mono<Void> removeSession(String sessionId);
}

View File

@@ -0,0 +1,4 @@
/**
* Web session support.
*/
package org.springframework.web.server.session;

View File

@@ -26,6 +26,7 @@ import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
@@ -165,6 +166,7 @@ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
return this.cachedContent.toByteArray();
}
// TODO: this is no longer usable in Servlet 3.0 environment
private class ContentCachingInputStream extends ServletInputStream {
@@ -182,6 +184,21 @@ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
}
return ch;
}
@Override
public boolean isFinished() {
return this.is.isFinished();
}
@Override
public boolean isReady() {
return this.is.isReady();
}
@Override
public void setReadListener(ReadListener readListener) {
this.setReadListener(readListener);
}
}
}

View File

@@ -21,8 +21,8 @@ import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
@@ -43,7 +43,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream();
private ServletOutputStream outputStream;
private PrintWriter writer;
@@ -109,6 +109,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (this.outputStream == null) {
this.outputStream = new ResponseServletOutputStream(getResponse().getOutputStream());
}
return this.outputStream;
}
@@ -225,9 +228,16 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
}
}
// TODO: this is no longer usable in Servlet 3.0 environment
private class ResponseServletOutputStream extends ServletOutputStream {
private final ServletOutputStream os;
public ResponseServletOutputStream(ServletOutputStream os) {
this.os = os;
}
@Override
public void write(int b) throws IOException {
content.write(b);
@@ -237,6 +247,16 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
public void write(byte[] b, int off, int len) throws IOException {
content.write(b, off, len);
}
@Override
public boolean isReady() {
return this.os.isReady();
}
@Override
public void setWriteListener(WriteListener writeListener) {
this.os.setWriteListener(writeListener);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
/**
* A helper class to obtain the lookup path for path matching purposes.
*
* @author Rossen Stoyanchev
*/
public class HttpRequestPathHelper {
private boolean urlDecode = true;
// TODO: sanitize path, default/request encoding?, remove path params?
/**
* Set if the request path should be URL-decoded.
* <p>Default is "true".
* @see UriUtils#decode(String, String)
*/
public void setUrlDecode(boolean urlDecode) {
this.urlDecode = urlDecode;
}
/**
* Whether the request path should be URL decoded.
*/
public boolean shouldUrlDecode() {
return this.urlDecode;
}
public String getLookupPathForRequest(ServerWebExchange exchange) {
String path = exchange.getRequest().getURI().getRawPath();
return (this.shouldUrlDecode() ? decode(exchange, path) : path);
}
private String decode(ServerWebExchange exchange, String path) {
// TODO: look up request encoding?
try {
return UriUtils.decode(path, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
// Should not happen
throw new IllegalStateException("Could not decode request string [" + path + "]");
}
}
/**
* Decode the given URI path variables unless {@link #setUrlDecode(boolean)}
* is set to {@code true} in which case it is assumed the URL path from
* which the variables were extracted is already decoded through a call to
* {@link #getLookupPathForRequest(ServerWebExchange)}.
* @param exchange current exchange
* @param vars URI variables extracted from the URL path
* @return the same Map or a new Map instance
*/
public Map<String, String> decodePathVariables(ServerWebExchange exchange, Map<String, String> vars) {
if (this.urlDecode) {
return vars;
}
Map<String, String> decodedVars = new LinkedHashMap<>(vars.size());
for (Map.Entry<String, String> entry : vars.entrySet()) {
decodedVars.put(entry.getKey(), decode(exchange, entry.getValue()));
}
return decodedVars;
}
/**
* Decode the given matrix variables unless {@link #setUrlDecode(boolean)}
* is set to {@code true} in which case it is assumed the URL path from
* which the variables were extracted is already decoded through a call to
* {@link #getLookupPathForRequest(ServerWebExchange)}.
* @param exchange current exchange
* @param vars URI variables extracted from the URL path
* @return the same Map or a new Map instance
*/
public MultiValueMap<String, String> decodeMatrixVariables(ServerWebExchange exchange,
MultiValueMap<String, String> vars) {
if (this.urlDecode) {
return vars;
}
MultiValueMap<String, String> decodedVars = new LinkedMultiValueMap<>(vars.size());
for (String key : vars.keySet()) {
for (String value : vars.get(key)) {
decodedVars.add(key, decode(exchange, value));
}
}
return decodedVars;
}
}