Move spring-web-reactive classes to spring-web
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Core package of the reactive server-side HTTP support.
|
||||
*/
|
||||
package org.springframework.http.server.reactive;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides WebHandler implementations.
|
||||
*/
|
||||
package org.springframework.web.server.handler;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Foundational Spring web server support.
|
||||
*/
|
||||
package org.springframework.web.server;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Web session support.
|
||||
*/
|
||||
package org.springframework.web.server.session;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user