diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java
new file mode 100644
index 0000000000..9499c8d81f
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java
@@ -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 Zero-copy
+ */
+public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage {
+
+ /**
+ * Set the body of the message to the given {@link File} which will be
+ * used to write 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 setBody(File file, long position, long count);
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java
new file mode 100644
index 0000000000..b55ad9c4cb
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java
@@ -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.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+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.DataBufferAllocator;
+import org.springframework.http.MediaType;
+import org.springframework.http.ReactiveHttpInputMessage;
+import org.springframework.http.ReactiveHttpOutputMessage;
+import org.springframework.http.support.MediaTypeUtils;
+
+/**
+ * @author Arjen Poutsma
+ */
+public class CodecHttpMessageConverter implements HttpMessageConverter {
+
+ private final Encoder encoder;
+
+ private final Decoder decoder;
+
+ public CodecHttpMessageConverter(Encoder encoder, Decoder decoder) {
+ this.encoder = encoder;
+ this.decoder = decoder;
+ }
+
+ @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 getReadableMediaTypes() {
+ return this.decoder != null ? this.decoder.getSupportedMimeTypes().stream().
+ map(MediaTypeUtils::toMediaType).
+ collect(Collectors.toList()) : Collections.emptyList();
+ }
+
+ @Override
+ public List getWritableMediaTypes() {
+ return this.encoder != null ? this.encoder.getSupportedMimeTypes().stream().
+ map(MediaTypeUtils::toMediaType).
+ collect(Collectors.toList()) : Collections.emptyList();
+ }
+
+ @Override
+ public Flux read(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
+ if (this.decoder == null) {
+ return Flux.error(new IllegalStateException("No decoder set"));
+ }
+ MediaType contentType = inputMessage.getHeaders().getContentType();
+ if (contentType == null) {
+ contentType = MediaType.APPLICATION_OCTET_STREAM;
+ }
+
+ Flux body = inputMessage.getBody();
+
+ return this.decoder.decode(body, type, contentType);
+ }
+
+ @Override
+ public Mono write(Publisher extends T> inputStream, ResolvableType type,
+ MediaType contentType,
+ ReactiveHttpOutputMessage outputMessage) {
+ if (this.encoder == null) {
+ return Mono.error(new IllegalStateException("No decoder set"));
+ }
+ outputMessage.getHeaders().setContentType(contentType);
+ DataBufferAllocator allocator = outputMessage.allocator();
+ Flux body = encoder.encode(inputStream, allocator, type, contentType);
+ return outputMessage.setBody(body);
+ }
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java
new file mode 100644
index 0000000000..ce001c5d08
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java
@@ -0,0 +1,91 @@
+/*
+ * 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 {
+
+ /**
+ * 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 getReadableMediaTypes();
+
+ /**
+ * Read an object 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 object
+ */
+ Flux read(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 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 write(Publisher extends T> inputStream,
+ ResolvableType type, MediaType contentType,
+ ReactiveHttpOutputMessage outputMessage);
+}
\ No newline at end of file
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java
new file mode 100644
index 0000000000..5091fc6fd2
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java
@@ -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.converter.reactive;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Collections;
+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.io.ByteArrayResource;
+import org.springframework.core.io.DescriptiveResource;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.support.DataBufferUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRangeResource;
+import org.springframework.http.MediaType;
+import org.springframework.http.ReactiveHttpInputMessage;
+import org.springframework.http.ReactiveHttpOutputMessage;
+import org.springframework.http.ZeroCopyHttpOutputMessage;
+import org.springframework.http.support.MediaTypeUtils;
+import org.springframework.util.MimeTypeUtils2;
+import org.springframework.util.ResourceUtils;
+import org.springframework.util.StreamUtils;
+
+/**
+ * @author Arjen Poutsma
+ */
+public class ResourceHttpMessageConverter implements HttpMessageConverter {
+
+ private static final int BUFFER_SIZE = StreamUtils.BUFFER_SIZE;
+
+ private static final List SUPPORTED_MEDIA_TYPES =
+ Collections.singletonList(MediaType.ALL);
+
+ @Override
+ public boolean canRead(ResolvableType type, MediaType mediaType) {
+ return Resource.class.isAssignableFrom(type.getRawClass());
+ }
+
+ @Override
+ public boolean canWrite(ResolvableType type, MediaType mediaType) {
+ return Resource.class.isAssignableFrom(type.getRawClass());
+ }
+
+ @Override
+ public List getReadableMediaTypes() {
+ return SUPPORTED_MEDIA_TYPES;
+ }
+
+ @Override
+ public List getWritableMediaTypes() {
+ return SUPPORTED_MEDIA_TYPES;
+ }
+
+ @Override
+ public Flux read(ResolvableType type,
+ ReactiveHttpInputMessage inputMessage) {
+ Class> clazz = type.getRawClass();
+
+ Flux body = inputMessage.getBody();
+
+ if (InputStreamResource.class.equals(clazz)) {
+ InputStream is = DataBufferUtils.toInputStream(body);
+ return Flux.just(new InputStreamResource(is));
+ }
+ else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
+ Mono singleBuffer = body.reduce(DataBuffer::write);
+ return Flux.from(singleBuffer.map(buffer -> {
+ byte[] bytes = new byte[buffer.readableByteCount()];
+ buffer.read(bytes);
+ return new ByteArrayResource(bytes);
+ }));
+ }
+ else {
+ return Flux.error(new IllegalStateException(
+ "Unsupported resource class: " + clazz));
+ }
+ }
+
+ @Override
+ public Mono write(Publisher extends Resource> inputStream,
+ ResolvableType type, MediaType contentType,
+ ReactiveHttpOutputMessage outputMessage) {
+
+ if (inputStream instanceof Mono) {
+ // single resource
+ return Mono.from(Flux.from(inputStream).
+ flatMap(resource -> {
+ HttpHeaders headers = outputMessage.getHeaders();
+ addHeaders(headers, resource, contentType);
+
+ if (resource instanceof HttpRangeResource) {
+ return writePartialContent((HttpRangeResource) resource,
+ outputMessage);
+ }
+ else {
+ return writeContent(resource, outputMessage, 0, -1);
+ }
+
+
+ }));
+ }
+ else {
+ // multiple resources, not supported!
+ return Mono.error(new IllegalArgumentException(
+ "Multiple resources not yet supported"));
+ }
+ }
+
+ 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 = MimeTypeUtils2.getMimeType(resource.getFilename()).
+ map(MediaTypeUtils::toMediaType).
+ orElse(MediaType.APPLICATION_OCTET_STREAM);
+ }
+ headers.setContentType(contentType);
+ }
+ if (headers.getContentLength() < 0) {
+ contentLength(resource).ifPresent(headers::setContentLength);
+ }
+ headers.add(HttpHeaders.ACCEPT_RANGES, "bytes");
+ }
+
+ private Mono writeContent(Resource resource,
+ ReactiveHttpOutputMessage outputMessage, long position, long count) {
+ if (outputMessage instanceof ZeroCopyHttpOutputMessage) {
+ Optional file = getFile(resource);
+ if (file.isPresent()) {
+ ZeroCopyHttpOutputMessage zeroCopyResponse =
+ (ZeroCopyHttpOutputMessage) outputMessage;
+
+ if (count < 0) {
+ count = file.get().length();
+ }
+
+ return zeroCopyResponse.setBody(file.get(), position, count);
+ }
+ }
+
+ // non-zero copy fallback
+ try {
+ InputStream is = resource.getInputStream();
+ long skipped = is.skip(position);
+ if (skipped < position) {
+ return Mono.error(new IOException(
+ "Skipped only " + skipped + " bytes out of " + count +
+ " required."));
+ }
+
+ Flux responseBody =
+ DataBufferUtils.read(is, outputMessage.allocator(), BUFFER_SIZE);
+ if (count > 0) {
+ responseBody = DataBufferUtils.takeUntilByteCount(responseBody, count);
+ }
+
+ return outputMessage.setBody(responseBody);
+ }
+ catch (IOException ex) {
+ return Mono.error(ex);
+ }
+ }
+
+ protected Mono writePartialContent(HttpRangeResource resource,
+ ReactiveHttpOutputMessage outputMessage) {
+
+ // TODO: implement
+
+ return Mono.empty();
+ }
+
+ private static Optional 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 getFile(Resource resource) {
+ // TODO: introduce Resource.hasFile() property to bypass the potential IOException thrown in Resource.getFile()
+ // the following Resource implementations do not support getURI/getFile
+ if (!(resource instanceof ByteArrayResource ||
+ resource instanceof DescriptiveResource ||
+ resource instanceof InputStreamResource)) {
+ try {
+ URI resourceUri = resource.getURI();
+ if (ResourceUtils.URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) {
+ return Optional.of(ResourceUtils.getFile(resourceUri));
+ }
+ }
+ catch (IOException ignored) {
+ }
+ }
+ return Optional.empty();
+ }
+
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java
index edf2a51a23..22041600e8 100644
--- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java
+++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java
@@ -94,7 +94,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
applyBeforeCommit().after(() -> setBodyInternal(writePublisher)));
}
- private Mono applyBeforeCommit() {
+ protected Mono applyBeforeCommit() {
Mono mono = Mono.empty();
if (this.state.compareAndSet(STATE_NEW, STATE_COMMITTING)) {
for (Supplier extends Mono> action : this.beforeCommitActions) {
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java
index 8eabb39314..98dbe3f7aa 100644
--- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java
+++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java
@@ -16,6 +16,8 @@
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;
@@ -31,6 +33,7 @@ import org.springframework.core.io.buffer.DataBufferAllocator;
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;
/**
@@ -39,7 +42,8 @@ import org.springframework.util.Assert;
* @author Stephane Maldini
* @author Rossen Stoyanchev
*/
-public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
+public class ReactorServerHttpResponse extends AbstractServerHttpResponse
+ implements ZeroCopyHttpOutputMessage {
private final HttpChannel channel;
@@ -99,4 +103,11 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse {
return Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
}
+
+ @Override
+ public Mono setBody(File file, long position, long count) {
+ return applyBeforeCommit().after(() -> {
+ return this.channel.sendFile(file, position, count);
+ });
+ }
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java
index a45c2b1b17..f22b794433 100644
--- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java
+++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java
@@ -104,4 +104,35 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
}
}
+/*
+ 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 setBody(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> header : getHeaders().entrySet()) {
+ String headerName = header.getKey();
+ for (String headerValue : header.getValue()) {
+ headers.add(headerName, headerValue);
+ }
+ }
+ Mono responseWrite = MonoChannelFuture.from(channel.write(httpResponse));
+
+ FileRegion fileRegion = new DefaultFileRegion(file, position, count);
+ Mono fileWrite = MonoChannelFuture.from(channel.writeAndFlush(fileRegion));
+
+ return Flux.concat(applyBeforeCommit(), responseWrite, fileWrite).after();
+ }
+*/
}
\ No newline at end of file
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
index 8acffe45bb..05cf650a5e 100644
--- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
+++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
@@ -67,9 +67,12 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
requestBody.registerListener();
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody);
- ResponseBodySubscriber responseBody = new ResponseBodySubscriber(exchange);
+ StreamSinkChannel responseChannel = exchange.getResponseChannel();
+ ResponseBodySubscriber responseBody =
+ new ResponseBodySubscriber(exchange, responseChannel);
responseBody.registerListener();
- ServerHttpResponse response = new UndertowServerHttpResponse(exchange,
+ ServerHttpResponse response =
+ new UndertowServerHttpResponse(exchange, responseChannel,
publisher -> Mono.from(subscriber -> publisher.subscribe(responseBody)),
allocator);
@@ -202,9 +205,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
private Subscription subscription;
- public ResponseBodySubscriber(HttpServerExchange exchange) {
+ public ResponseBodySubscriber(HttpServerExchange exchange,
+ StreamSinkChannel responseChannel) {
this.exchange = exchange;
- this.responseChannel = exchange.getResponseChannel();
+ this.responseChannel = responseChannel;
}
public void registerListener() {
diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java
index 1d3d1598da..67dd0a893c 100644
--- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java
+++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java
@@ -16,6 +16,10 @@
package org.springframework.http.server.reactive;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -25,12 +29,14 @@ import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import io.undertow.util.HttpString;
import org.reactivestreams.Publisher;
+import org.xnio.channels.StreamSinkChannel;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferAllocator;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
+import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.Assert;
/**
@@ -39,19 +45,25 @@ import org.springframework.util.Assert;
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
*/
-public class UndertowServerHttpResponse extends AbstractServerHttpResponse {
+public class UndertowServerHttpResponse extends AbstractServerHttpResponse
+ implements ZeroCopyHttpOutputMessage {
private final HttpServerExchange exchange;
+ private final StreamSinkChannel responseChannel;
+
private final Function, Mono> responseBodyWriter;
public UndertowServerHttpResponse(HttpServerExchange exchange,
+ StreamSinkChannel responseChannel,
Function, Mono> responseBodyWriter,
DataBufferAllocator allocator) {
super(allocator);
Assert.notNull(exchange, "'exchange' is required.");
+ Assert.notNull(responseChannel, "'responseChannel' must not be null");
Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null");
this.exchange = exchange;
+ this.responseChannel = responseChannel;
this.responseBodyWriter = responseBodyWriter;
}
@@ -71,6 +83,26 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse {
return this.responseBodyWriter.apply(publisher);
}
+ @Override
+ public Mono setBody(File file, long position, long count) {
+ writeHeaders();
+ writeCookies();
+ try {
+ FileChannel in = new FileInputStream(file).getChannel();
+ long result = this.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> entry : getHeaders().entrySet()) {
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java
index 57b98d666f..2326797943 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java
@@ -13,23 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.web.reactive.accept;
-import java.io.IOException;
-import java.io.InputStream;
import java.util.Locale;
import java.util.Map;
-import javax.activation.FileTypeMap;
-import javax.activation.MimetypesFileTypeMap;
+import java.util.Optional;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
+import org.springframework.http.support.MediaTypeUtils;
import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils2;
import org.springframework.util.StringUtils;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
@@ -48,12 +44,6 @@ import org.springframework.web.util.WebUtils;
*/
public class PathExtensionContentTypeResolver extends AbstractMappingContentTypeResolver {
- private static final Log logger = LogFactory.getLog(PathExtensionContentTypeResolver.class);
-
- private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap",
- PathExtensionContentTypeResolver.class.getClassLoader());
-
-
private boolean useJaf = true;
private boolean ignoreUnknownExtensions = true;
@@ -103,8 +93,9 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType
@Override
protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException {
- if (this.useJaf && JAF_PRESENT) {
- MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + key);
+ if (this.useJaf) {
+ Optional mimeType = MimeTypeUtils2.getMimeType("file." + key);
+ MediaType mediaType = mimeType.map(MediaTypeUtils::toMediaType).orElse(null);
if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
return mediaType;
}
@@ -130,8 +121,10 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType
if (extension != null) {
mediaType = getMediaType(extension);
}
- if (mediaType == null && JAF_PRESENT) {
- mediaType = JafMediaTypeFactory.getMediaType(filename);
+ if (mediaType == null) {
+ mediaType =
+ MimeTypeUtils2.getMimeType(filename).map(MediaTypeUtils::toMediaType)
+ .orElse(null);
}
if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
mediaType = null;
@@ -139,56 +132,4 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType
return mediaType;
}
-
- /**
- * Inner class to avoid hard-coded dependency on JAF.
- */
- private static class JafMediaTypeFactory {
-
- private static final FileTypeMap fileTypeMap;
-
- static {
- fileTypeMap = initFileTypeMap();
- }
-
- /**
- * Find extended mime.types from the spring-context-support module.
- */
- private static FileTypeMap initFileTypeMap() {
- Resource resource = new ClassPathResource("org/springframework/mail/javamail/mime.types");
- if (resource.exists()) {
- if (logger.isTraceEnabled()) {
- logger.trace("Loading JAF FileTypeMap from " + resource);
- }
- InputStream inputStream = null;
- try {
- inputStream = resource.getInputStream();
- return new MimetypesFileTypeMap(inputStream);
- }
- catch (IOException ex) {
- // ignore
- }
- finally {
- if (inputStream != null) {
- try {
- inputStream.close();
- }
- catch (IOException ex) {
- // ignore
- }
- }
- }
- }
- if (logger.isTraceEnabled()) {
- logger.trace("Loading default Java Activation Framework FileTypeMap");
- }
- return FileTypeMap.getDefaultFileTypeMap();
- }
-
- public static MediaType getMediaType(String filename) {
- String mediaType = fileTypeMap.getContentType(filename);
- return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null);
- }
- }
-
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
index 216a62cc4c..fca79df098 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
@@ -24,10 +24,10 @@ import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
-import org.springframework.core.codec.Decoder;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
+import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestBody;
@@ -40,15 +40,15 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
- private final List> decoders;
+ private final List> messageConverters;
private final ConversionService conversionService;
-
- public RequestBodyArgumentResolver(List> decoders, ConversionService service) {
- Assert.notEmpty(decoders, "At least one decoder is required.");
+ public RequestBodyArgumentResolver(List> messageConverters,
+ ConversionService service) {
+ Assert.notEmpty(messageConverters, "At least one message converter is required.");
Assert.notNull(service, "'conversionService' is required.");
- this.decoders = decoders;
+ this.messageConverters = messageConverters;
this.conversionService = service;
}
@@ -62,22 +62,29 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
public Mono