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}