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}