001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.codec;
018
019import java.io.File;
020import java.io.IOException;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024
025import org.apache.commons.logging.Log;
026import org.reactivestreams.Publisher;
027import reactor.core.publisher.Flux;
028import reactor.core.publisher.Mono;
029
030import org.springframework.core.ResolvableType;
031import org.springframework.core.codec.Hints;
032import org.springframework.core.codec.ResourceDecoder;
033import org.springframework.core.codec.ResourceEncoder;
034import org.springframework.core.codec.ResourceRegionEncoder;
035import org.springframework.core.io.InputStreamResource;
036import org.springframework.core.io.Resource;
037import org.springframework.core.io.buffer.DataBuffer;
038import org.springframework.core.io.buffer.DataBufferFactory;
039import org.springframework.core.io.support.ResourceRegion;
040import org.springframework.http.HttpHeaders;
041import org.springframework.http.HttpLogging;
042import org.springframework.http.HttpRange;
043import org.springframework.http.HttpStatus;
044import org.springframework.http.MediaType;
045import org.springframework.http.MediaTypeFactory;
046import org.springframework.http.ReactiveHttpOutputMessage;
047import org.springframework.http.ZeroCopyHttpOutputMessage;
048import org.springframework.http.server.reactive.ServerHttpRequest;
049import org.springframework.http.server.reactive.ServerHttpResponse;
050import org.springframework.lang.Nullable;
051import org.springframework.util.MimeTypeUtils;
052
053/**
054 * {@code HttpMessageWriter} that can write a {@link Resource}.
055 *
056 * <p>Also an implementation of {@code HttpMessageWriter} with support for writing one
057 * or more {@link ResourceRegion}'s based on the HTTP ranges specified in the request.
058 *
059 * <p>For reading to a Resource, use {@link ResourceDecoder} wrapped with
060 * {@link DecoderHttpMessageReader}.
061 *
062 * @author Arjen Poutsma
063 * @author Brian Clozel
064 * @author Rossen Stoyanchev
065 * @since 5.0
066 * @see ResourceEncoder
067 * @see ResourceRegionEncoder
068 * @see HttpRange
069 */
070public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
071
072        private static final ResolvableType REGION_TYPE = ResolvableType.forClass(ResourceRegion.class);
073
074        private static final Log logger = HttpLogging.forLogName(ResourceHttpMessageWriter.class);
075
076
077        private final ResourceEncoder encoder;
078
079        private final ResourceRegionEncoder regionEncoder;
080
081        private final List<MediaType> mediaTypes;
082
083
084        public ResourceHttpMessageWriter() {
085                this(ResourceEncoder.DEFAULT_BUFFER_SIZE);
086        }
087
088        public ResourceHttpMessageWriter(int bufferSize) {
089                this.encoder = new ResourceEncoder(bufferSize);
090                this.regionEncoder = new ResourceRegionEncoder(bufferSize);
091                this.mediaTypes = MediaType.asMediaTypes(this.encoder.getEncodableMimeTypes());
092        }
093
094
095        @Override
096        public boolean canWrite(ResolvableType elementType, @Nullable MediaType mediaType) {
097                return this.encoder.canEncode(elementType, mediaType);
098        }
099
100        @Override
101        public List<MediaType> getWritableMediaTypes() {
102                return this.mediaTypes;
103        }
104
105
106        // Client or server: single Resource...
107
108        @Override
109        public Mono<Void> write(Publisher<? extends Resource> inputStream, ResolvableType elementType,
110                        @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map<String, Object> hints) {
111
112                return Mono.from(inputStream).flatMap(resource ->
113                                writeResource(resource, elementType, mediaType, message, hints));
114        }
115
116        private Mono<Void> writeResource(Resource resource, ResolvableType type, @Nullable MediaType mediaType,
117                        ReactiveHttpOutputMessage message, Map<String, Object> hints) {
118
119                HttpHeaders headers = message.getHeaders();
120                MediaType resourceMediaType = getResourceMediaType(mediaType, resource, hints);
121                headers.setContentType(resourceMediaType);
122
123                if (headers.getContentLength() < 0) {
124                        long length = lengthOf(resource);
125                        if (length != -1) {
126                                headers.setContentLength(length);
127                        }
128                }
129
130                return zeroCopy(resource, null, message, hints)
131                                .orElseGet(() -> {
132                                        Mono<Resource> input = Mono.just(resource);
133                                        DataBufferFactory factory = message.bufferFactory();
134                                        Flux<DataBuffer> body = this.encoder.encode(input, factory, type, resourceMediaType, hints);
135                                        return message.writeWith(body);
136                                });
137        }
138
139        private static MediaType getResourceMediaType(
140                        @Nullable MediaType mediaType, Resource resource, Map<String, Object> hints) {
141
142                if (mediaType != null && mediaType.isConcrete() && !mediaType.equals(MediaType.APPLICATION_OCTET_STREAM)) {
143                        return mediaType;
144                }
145                mediaType = MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
146                if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) {
147                        logger.debug(Hints.getLogPrefix(hints) + "Resource associated with '" + mediaType + "'");
148                }
149                return mediaType;
150        }
151
152        private static long lengthOf(Resource resource) {
153                // Don't consume InputStream...
154                if (InputStreamResource.class != resource.getClass()) {
155                        try {
156                                return resource.contentLength();
157                        }
158                        catch (IOException ignored) {
159                        }
160                }
161                return -1;
162        }
163
164        private static Optional<Mono<Void>> zeroCopy(Resource resource, @Nullable ResourceRegion region,
165                        ReactiveHttpOutputMessage message, Map<String, Object> hints) {
166
167                if (message instanceof ZeroCopyHttpOutputMessage && resource.isFile()) {
168                        try {
169                                File file = resource.getFile();
170                                long pos = region != null ? region.getPosition() : 0;
171                                long count = region != null ? region.getCount() : file.length();
172                                if (logger.isDebugEnabled()) {
173                                        String formatted = region != null ? "region " + pos + "-" + (count) + " of " : "";
174                                        logger.debug(Hints.getLogPrefix(hints) + "Zero-copy " + formatted + "[" + resource + "]");
175                                }
176                                return Optional.of(((ZeroCopyHttpOutputMessage) message).writeWith(file, pos, count));
177                        }
178                        catch (IOException ex) {
179                                // should not happen
180                        }
181                }
182                return Optional.empty();
183        }
184
185
186        // Server-side only: single Resource or sub-regions...
187
188        @Override
189        public Mono<Void> write(Publisher<? extends Resource> inputStream, @Nullable ResolvableType actualType,
190                        ResolvableType elementType, @Nullable MediaType mediaType, ServerHttpRequest request,
191                        ServerHttpResponse response, Map<String, Object> hints) {
192
193                HttpHeaders headers = response.getHeaders();
194                headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
195
196                List<HttpRange> ranges;
197                try {
198                        ranges = request.getHeaders().getRange();
199                }
200                catch (IllegalArgumentException ex) {
201                        response.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
202                        return response.setComplete();
203                }
204
205                return Mono.from(inputStream).flatMap(resource -> {
206                        if (ranges.isEmpty()) {
207                                return writeResource(resource, elementType, mediaType, response, hints);
208                        }
209                        response.setStatusCode(HttpStatus.PARTIAL_CONTENT);
210                        List<ResourceRegion> regions = HttpRange.toResourceRegions(ranges, resource);
211                        MediaType resourceMediaType = getResourceMediaType(mediaType, resource, hints);
212                        if (regions.size() == 1){
213                                ResourceRegion region = regions.get(0);
214                                headers.setContentType(resourceMediaType);
215                                long contentLength = lengthOf(resource);
216                                if (contentLength != -1) {
217                                        long start = region.getPosition();
218                                        long end = start + region.getCount() - 1;
219                                        end = Math.min(end, contentLength - 1);
220                                        headers.add("Content-Range", "bytes " + start + '-' + end + '/' + contentLength);
221                                        headers.setContentLength(end - start + 1);
222                                }
223                                return writeSingleRegion(region, response, hints);
224                        }
225                        else {
226                                String boundary = MimeTypeUtils.generateMultipartBoundaryString();
227                                MediaType multipartType = MediaType.parseMediaType("multipart/byteranges;boundary=" + boundary);
228                                headers.setContentType(multipartType);
229                                Map<String, Object> allHints = Hints.merge(hints, ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary);
230                                return encodeAndWriteRegions(Flux.fromIterable(regions), resourceMediaType, response, allHints);
231                        }
232                });
233        }
234
235        private Mono<Void> writeSingleRegion(ResourceRegion region, ReactiveHttpOutputMessage message,
236                        Map<String, Object> hints) {
237
238                return zeroCopy(region.getResource(), region, message, hints)
239                                .orElseGet(() -> {
240                                        Publisher<? extends ResourceRegion> input = Mono.just(region);
241                                        MediaType mediaType = message.getHeaders().getContentType();
242                                        return encodeAndWriteRegions(input, mediaType, message, hints);
243                                });
244        }
245
246        private Mono<Void> encodeAndWriteRegions(Publisher<? extends ResourceRegion> publisher,
247                        @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map<String, Object> hints) {
248
249                Flux<DataBuffer> body = this.regionEncoder.encode(
250                                publisher, message.bufferFactory(), REGION_TYPE, mediaType, hints);
251
252                return message.writeWith(body);
253        }
254
255}