diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java index 8dc9322d9a..23ae1109ce 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -55,4 +55,8 @@ public class ByteArrayEncoder extends AbstractEncoder { return Flux.from(inputStream).map(bufferFactory::wrap); } + @Override + public Long getContentLength(byte[] bytes, @Nullable MimeType mimeType) { + return (long) bytes.length; + } } diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java index 27f1410c26..065c322221 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -56,4 +56,8 @@ public class ByteBufferEncoder extends AbstractEncoder { return Flux.from(inputStream).map(bufferFactory::wrap); } + @Override + public Long getContentLength(ByteBuffer byteBuffer, @Nullable MimeType mimeType) { + return (long) byteBuffer.array().length; + } } diff --git a/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java index ffda0dd1fa..aa852229d0 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -62,13 +62,8 @@ public class CharSequenceEncoder extends AbstractEncoder { DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - Charset charset; - if (mimeType != null && mimeType.getCharset() != null) { - charset = mimeType.getCharset(); - } - else { - charset = DEFAULT_CHARSET; - } + Charset charset = getCharset(mimeType); + return Flux.from(inputStream).map(charSequence -> { CharBuffer charBuffer = CharBuffer.wrap(charSequence); ByteBuffer byteBuffer = charset.encode(charBuffer); @@ -76,6 +71,21 @@ public class CharSequenceEncoder extends AbstractEncoder { }); } + private Charset getCharset(@Nullable MimeType mimeType) { + Charset charset; + if (mimeType != null && mimeType.getCharset() != null) { + charset = mimeType.getCharset(); + } + else { + charset = DEFAULT_CHARSET; + } + return charset; + } + + @Override + public Long getContentLength(CharSequence data, @Nullable MimeType mimeType) { + return (long) data.toString().getBytes(getCharset(mimeType)).length; + } /** * Create a {@code CharSequenceEncoder} that supports only "text/plain". diff --git a/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java index dd2d25207d..46d01f893c 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -55,4 +55,8 @@ public class DataBufferEncoder extends AbstractEncoder { return Flux.from(inputStream); } + @Override + public Long getContentLength(DataBuffer dataBuffer, MimeType mimeType) { + return (long) dataBuffer.readableByteCount(); + } } diff --git a/spring-core/src/main/java/org/springframework/core/codec/Encoder.java b/spring-core/src/main/java/org/springframework/core/codec/Encoder.java index 5d9e493e48..6522385b6b 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Encoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -67,6 +67,17 @@ public interface Encoder { Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints); + /** + * Return the length for the given item, if known. + * @param t the item to check + * @return the length in bytes, or {@code null} if not known. + * @since 5.0.5 + */ + @Nullable + default Long getContentLength(T t, @Nullable MimeType mimeType) { + return null; + } + /** * Return the list of mime types this encoder supports. */ diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java index 499245598e..85c517f9d2 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,14 @@ package org.springframework.core.codec; +import java.io.IOException; import java.util.Map; +import java.util.OptionalLong; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +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.DataBufferFactory; @@ -68,4 +71,17 @@ public class ResourceEncoder extends AbstractSingleValueEncoder { return DataBufferUtils.read(resource, dataBufferFactory, this.bufferSize); } + @Override + public Long getContentLength(Resource resource, @Nullable MimeType mimeType) { + // Don't consume InputStream... + if (InputStreamResource.class != resource.getClass()) { + try { + return resource.contentLength(); + } + catch (IOException ignored) { + } + } + return null; + } + } diff --git a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java index 9366f2c956..02b2e1869e 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -91,11 +92,25 @@ public class EncoderHttpMessageWriter implements HttpMessageWriter { return this.encoder.canEncode(elementType, mediaType); } + @SuppressWarnings("unchecked") @Override public Mono write(Publisher inputStream, ResolvableType elementType, @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map hints) { MediaType contentType = updateContentType(message, mediaType); + HttpHeaders headers = message.getHeaders(); + + if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { + if (inputStream instanceof Mono) { + // This works because we don't actually commit until after the first signal... + inputStream = ((Mono) inputStream).doOnNext(data -> { + Long contentLength = this.encoder.getContentLength(data, contentType); + if (contentLength != null) { + headers.setContentLength(contentLength); + } + }); + } + } Flux body = this.encoder.encode( inputStream, message.bufferFactory(), elementType, contentType, hints); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java index b1d83cce4a..1ea802c063 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.OptionalLong; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -32,7 +31,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.ResourceEncoder; import org.springframework.core.codec.ResourceRegionEncoder; -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.DataBufferFactory; @@ -49,7 +47,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.MimeTypeUtils; -import static java.util.Collections.emptyMap; +import static java.util.Collections.*; /** * {@code HttpMessageWriter} that can write a {@link Resource}. @@ -121,7 +119,10 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter { headers.setContentType(resourceMediaType); if (headers.getContentLength() < 0) { - lengthOf(resource).ifPresent(headers::setContentLength); + Long contentLength = this.encoder.getContentLength(resource, mediaType); + if (contentLength != null) { + headers.setContentLength(contentLength); + } } return zeroCopy(resource, null, message) @@ -140,18 +141,6 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter { return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); } - private static OptionalLong lengthOf(Resource resource) { - // Don't consume InputStream... - if (InputStreamResource.class != resource.getClass()) { - try { - return OptionalLong.of(resource.contentLength()); - } - catch (IOException ignored) { - } - } - return OptionalLong.empty(); - } - private static Optional> zeroCopy(Resource resource, @Nullable ResourceRegion region, ReactiveHttpOutputMessage message) { @@ -205,13 +194,14 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter { if (regions.size() == 1){ ResourceRegion region = regions.get(0); headers.setContentType(resourceMediaType); - lengthOf(resource).ifPresent(length -> { + Long contentLength = this.encoder.getContentLength(resource, mediaType); + if (contentLength != null) { long start = region.getPosition(); long end = start + region.getCount() - 1; - end = Math.min(end, length - 1); - headers.add("Content-Range", "bytes " + start + '-' + end + '/' + length); + end = Math.min(end, contentLength - 1); + headers.add("Content-Range", "bytes " + start + '-' + end + '/' + contentLength); headers.setContentLength(end - start + 1); - }); + } return writeSingleRegion(region, response); } else { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java index 6850f3f35e..88440f9e02 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -59,10 +59,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.springframework.http.MediaType.APPLICATION_XML; +import static java.util.Arrays.*; +import static org.junit.Assert.*; +import static org.springframework.http.MediaType.*; /** * {@code @RequestMapping} integration tests focusing on serialization and @@ -87,7 +86,6 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq return wac; } - @Test public void byteBufferResponseBodyWithPublisher() throws Exception { Person expected = new Person("Robert"); @@ -100,6 +98,14 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq assertEquals(expected, performGet("/raw-response/flux", new HttpHeaders(), String.class).getBody()); } + @Test + public void byteBufferResponseBodyWithMono() throws Exception { + String expected = "Hello!"; + ResponseEntity responseEntity = performGet("/raw-response/mono", new HttpHeaders(), String.class); + assertEquals(6, responseEntity.getHeaders().getContentLength()); + assertEquals(expected, responseEntity.getBody()); + } + @Test public void byteBufferResponseBodyWithObservable() throws Exception { String expected = "Hello!"; @@ -422,6 +428,11 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); } + @GetMapping("/mono") + public Mono getMonoString() { + return Mono.just(ByteBuffer.wrap("Hello!".getBytes())); + } + @GetMapping("/observable") public Observable getObservable() { return Observable.just(ByteBuffer.wrap("Hello!".getBytes()));