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.core.codec; 018 019import java.io.IOException; 020import java.nio.charset.StandardCharsets; 021import java.util.Map; 022import java.util.OptionalLong; 023 024import org.reactivestreams.Publisher; 025import reactor.core.publisher.Flux; 026import reactor.core.publisher.Mono; 027 028import org.springframework.core.ResolvableType; 029import org.springframework.core.io.InputStreamResource; 030import org.springframework.core.io.Resource; 031import org.springframework.core.io.buffer.DataBuffer; 032import org.springframework.core.io.buffer.DataBufferFactory; 033import org.springframework.core.io.buffer.DataBufferUtils; 034import org.springframework.core.io.support.ResourceRegion; 035import org.springframework.lang.Nullable; 036import org.springframework.util.Assert; 037import org.springframework.util.MimeType; 038import org.springframework.util.MimeTypeUtils; 039import org.springframework.util.StreamUtils; 040 041/** 042 * Encoder for {@link ResourceRegion ResourceRegions}. 043 * 044 * @author Brian Clozel 045 * @since 5.0 046 */ 047public class ResourceRegionEncoder extends AbstractEncoder<ResourceRegion> { 048 049 /** 050 * The default buffer size used by the encoder. 051 */ 052 public static final int DEFAULT_BUFFER_SIZE = StreamUtils.BUFFER_SIZE; 053 054 /** 055 * The hint key that contains the boundary string. 056 */ 057 public static final String BOUNDARY_STRING_HINT = ResourceRegionEncoder.class.getName() + ".boundaryString"; 058 059 private final int bufferSize; 060 061 062 public ResourceRegionEncoder() { 063 this(DEFAULT_BUFFER_SIZE); 064 } 065 066 public ResourceRegionEncoder(int bufferSize) { 067 super(MimeTypeUtils.APPLICATION_OCTET_STREAM, MimeTypeUtils.ALL); 068 Assert.isTrue(bufferSize > 0, "'bufferSize' must be larger than 0"); 069 this.bufferSize = bufferSize; 070 } 071 072 @Override 073 public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { 074 return super.canEncode(elementType, mimeType) 075 && ResourceRegion.class.isAssignableFrom(elementType.toClass()); 076 } 077 078 @Override 079 public Flux<DataBuffer> encode(Publisher<? extends ResourceRegion> input, 080 DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, 081 @Nullable Map<String, Object> hints) { 082 083 Assert.notNull(input, "'inputStream' must not be null"); 084 Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); 085 Assert.notNull(elementType, "'elementType' must not be null"); 086 087 if (input instanceof Mono) { 088 return Mono.from(input) 089 .flatMapMany(region -> { 090 if (!region.getResource().isReadable()) { 091 return Flux.error(new EncodingException( 092 "Resource " + region.getResource() + " is not readable")); 093 } 094 return writeResourceRegion(region, bufferFactory, hints); 095 }); 096 } 097 else { 098 final String boundaryString = Hints.getRequiredHint(hints, BOUNDARY_STRING_HINT); 099 byte[] startBoundary = toAsciiBytes("\r\n--" + boundaryString + "\r\n"); 100 byte[] contentType = mimeType != null ? toAsciiBytes("Content-Type: " + mimeType + "\r\n") : new byte[0]; 101 102 return Flux.from(input) 103 .concatMap(region -> { 104 if (!region.getResource().isReadable()) { 105 return Flux.error(new EncodingException( 106 "Resource " + region.getResource() + " is not readable")); 107 } 108 Flux<DataBuffer> prefix = Flux.just( 109 bufferFactory.wrap(startBoundary), 110 bufferFactory.wrap(contentType), 111 bufferFactory.wrap(getContentRangeHeader(region))); // only wrapping, no allocation 112 113 return prefix.concatWith(writeResourceRegion(region, bufferFactory, hints)); 114 }) 115 .concatWithValues(getRegionSuffix(bufferFactory, boundaryString)); 116 } 117 // No doOnDiscard (no caching after DataBufferUtils#read) 118 } 119 120 private Flux<DataBuffer> writeResourceRegion( 121 ResourceRegion region, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints) { 122 123 Resource resource = region.getResource(); 124 long position = region.getPosition(); 125 long count = region.getCount(); 126 127 if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { 128 logger.debug(Hints.getLogPrefix(hints) + 129 "Writing region " + position + "-" + (position + count) + " of [" + resource + "]"); 130 } 131 132 Flux<DataBuffer> in = DataBufferUtils.read(resource, position, bufferFactory, this.bufferSize); 133 return DataBufferUtils.takeUntilByteCount(in, count); 134 } 135 136 private DataBuffer getRegionSuffix(DataBufferFactory bufferFactory, String boundaryString) { 137 byte[] endBoundary = toAsciiBytes("\r\n--" + boundaryString + "--"); 138 return bufferFactory.wrap(endBoundary); 139 } 140 141 private byte[] toAsciiBytes(String in) { 142 return in.getBytes(StandardCharsets.US_ASCII); 143 } 144 145 private byte[] getContentRangeHeader(ResourceRegion region) { 146 long start = region.getPosition(); 147 long end = start + region.getCount() - 1; 148 OptionalLong contentLength = contentLength(region.getResource()); 149 if (contentLength.isPresent()) { 150 long length = contentLength.getAsLong(); 151 return toAsciiBytes("Content-Range: bytes " + start + '-' + end + '/' + length + "\r\n\r\n"); 152 } 153 else { 154 return toAsciiBytes("Content-Range: bytes " + start + '-' + end + "\r\n\r\n"); 155 } 156 } 157 158 /** 159 * Determine, if possible, the contentLength of the given resource without reading it. 160 * @param resource the resource instance 161 * @return the contentLength of the resource 162 */ 163 private OptionalLong contentLength(Resource resource) { 164 // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... 165 // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! 166 if (InputStreamResource.class != resource.getClass()) { 167 try { 168 return OptionalLong.of(resource.contentLength()); 169 } 170 catch (IOException ignored) { 171 } 172 } 173 return OptionalLong.empty(); 174 } 175 176}