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;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Iterator;
024import java.util.List;
025
026import org.springframework.core.io.InputStreamResource;
027import org.springframework.core.io.Resource;
028import org.springframework.core.io.support.ResourceRegion;
029import org.springframework.util.Assert;
030import org.springframework.util.CollectionUtils;
031import org.springframework.util.ObjectUtils;
032import org.springframework.util.StringUtils;
033
034/**
035 * Represents an HTTP (byte) range for use with the HTTP {@code "Range"} header.
036 *
037 * @author Arjen Poutsma
038 * @author Juergen Hoeller
039 * @since 4.2
040 * @see <a href="https://tools.ietf.org/html/rfc7233">HTTP/1.1: Range Requests</a>
041 * @see HttpHeaders#setRange(List)
042 * @see HttpHeaders#getRange()
043 */
044public abstract class HttpRange {
045
046        /** Maximum ranges per request. */
047        private static final int MAX_RANGES = 100;
048
049        private static final String BYTE_RANGE_PREFIX = "bytes=";
050
051
052        /**
053         * Turn a {@code Resource} into a {@link ResourceRegion} using the range
054         * information contained in the current {@code HttpRange}.
055         * @param resource the {@code Resource} to select the region from
056         * @return the selected region of the given {@code Resource}
057         * @since 4.3
058         */
059        public ResourceRegion toResourceRegion(Resource resource) {
060                // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
061                // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
062                Assert.isTrue(resource.getClass() != InputStreamResource.class,
063                                "Cannot convert an InputStreamResource to a ResourceRegion");
064                long contentLength = getLengthFor(resource);
065                long start = getRangeStart(contentLength);
066                long end = getRangeEnd(contentLength);
067                return new ResourceRegion(resource, start, end - start + 1);
068        }
069
070        /**
071         * Return the start of the range given the total length of a representation.
072         * @param length the length of the representation
073         * @return the start of this range for the representation
074         */
075        public abstract long getRangeStart(long length);
076
077        /**
078         * Return the end of the range (inclusive) given the total length of a representation.
079         * @param length the length of the representation
080         * @return the end of the range for the representation
081         */
082        public abstract long getRangeEnd(long length);
083
084
085        /**
086         * Create an {@code HttpRange} from the given position to the end.
087         * @param firstBytePos the first byte position
088         * @return a byte range that ranges from {@code firstPos} till the end
089         * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
090         */
091        public static HttpRange createByteRange(long firstBytePos) {
092                return new ByteRange(firstBytePos, null);
093        }
094
095        /**
096         * Create a {@code HttpRange} from the given fist to last position.
097         * @param firstBytePos the first byte position
098         * @param lastBytePos the last byte position
099         * @return a byte range that ranges from {@code firstPos} till {@code lastPos}
100         * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
101         */
102        public static HttpRange createByteRange(long firstBytePos, long lastBytePos) {
103                return new ByteRange(firstBytePos, lastBytePos);
104        }
105
106        /**
107         * Create an {@code HttpRange} that ranges over the last given number of bytes.
108         * @param suffixLength the number of bytes for the range
109         * @return a byte range that ranges over the last {@code suffixLength} number of bytes
110         * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
111         */
112        public static HttpRange createSuffixRange(long suffixLength) {
113                return new SuffixByteRange(suffixLength);
114        }
115
116        /**
117         * Parse the given, comma-separated string into a list of {@code HttpRange} objects.
118         * <p>This method can be used to parse an {@code Range} header.
119         * @param ranges the string to parse
120         * @return the list of ranges
121         * @throws IllegalArgumentException if the string cannot be parsed
122         * or if the number of ranges is greater than 100
123         */
124        public static List<HttpRange> parseRanges(String ranges) {
125                if (!StringUtils.hasLength(ranges)) {
126                        return Collections.emptyList();
127                }
128                if (!ranges.startsWith(BYTE_RANGE_PREFIX)) {
129                        throw new IllegalArgumentException("Range '" + ranges + "' does not start with 'bytes='");
130                }
131                ranges = ranges.substring(BYTE_RANGE_PREFIX.length());
132
133                String[] tokens = StringUtils.tokenizeToStringArray(ranges, ",");
134                if (tokens.length > MAX_RANGES) {
135                        throw new IllegalArgumentException("Too many ranges: " + tokens.length);
136                }
137                List<HttpRange> result = new ArrayList<HttpRange>(tokens.length);
138                for (String token : tokens) {
139                        result.add(parseRange(token));
140                }
141                return result;
142        }
143
144        private static HttpRange parseRange(String range) {
145                Assert.hasLength(range, "Range String must not be empty");
146                int dashIdx = range.indexOf('-');
147                if (dashIdx > 0) {
148                        long firstPos = Long.parseLong(range.substring(0, dashIdx));
149                        if (dashIdx < range.length() - 1) {
150                                Long lastPos = Long.parseLong(range.substring(dashIdx + 1));
151                                return new ByteRange(firstPos, lastPos);
152                        }
153                        else {
154                                return new ByteRange(firstPos, null);
155                        }
156                }
157                else if (dashIdx == 0) {
158                        long suffixLength = Long.parseLong(range.substring(1));
159                        return new SuffixByteRange(suffixLength);
160                }
161                else {
162                        throw new IllegalArgumentException("Range '" + range + "' does not contain \"-\"");
163                }
164        }
165
166        /**
167         * Convert each {@code HttpRange} into a {@code ResourceRegion}, selecting the
168         * appropriate segment of the given {@code Resource} using HTTP Range information.
169         * @param ranges the list of ranges
170         * @param resource the resource to select the regions from
171         * @return the list of regions for the given resource
172         * @throws IllegalArgumentException if the sum of all ranges exceeds the resource length
173         * @since 4.3
174         */
175        public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) {
176                if (CollectionUtils.isEmpty(ranges)) {
177                        return Collections.emptyList();
178                }
179                List<ResourceRegion> regions = new ArrayList<ResourceRegion>(ranges.size());
180                for (HttpRange range : ranges) {
181                        regions.add(range.toResourceRegion(resource));
182                }
183                if (ranges.size() > 1) {
184                        long length = getLengthFor(resource);
185                        long total = 0;
186                        for (ResourceRegion region : regions) {
187                                total += region.getCount();
188                        }
189                        if (total >= length) {
190                                throw new IllegalArgumentException("The sum of all ranges (" + total +
191                                                ") should be less than the resource length (" + length + ")");
192                        }
193                }
194                return regions;
195        }
196
197        private static long getLengthFor(Resource resource) {
198                try {
199                        long contentLength = resource.contentLength();
200                        Assert.isTrue(contentLength > 0, "Resource content length should be > 0");
201                        return contentLength;
202                }
203                catch (IOException ex) {
204                        throw new IllegalArgumentException("Failed to obtain Resource content length", ex);
205                }
206        }
207
208        /**
209         * Return a string representation of the given list of {@code HttpRange} objects.
210         * <p>This method can be used to for an {@code Range} header.
211         * @param ranges the ranges to create a string of
212         * @return the string representation
213         */
214        public static String toString(Collection<HttpRange> ranges) {
215                Assert.notEmpty(ranges, "Ranges Collection must not be empty");
216                StringBuilder builder = new StringBuilder(BYTE_RANGE_PREFIX);
217                for (Iterator<HttpRange> iterator = ranges.iterator(); iterator.hasNext(); ) {
218                        HttpRange range = iterator.next();
219                        builder.append(range);
220                        if (iterator.hasNext()) {
221                                builder.append(", ");
222                        }
223                }
224                return builder.toString();
225        }
226
227
228        /**
229         * Represents an HTTP/1.1 byte range, with a first and optional last position.
230         * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
231         * @see HttpRange#createByteRange(long)
232         * @see HttpRange#createByteRange(long, long)
233         */
234        private static class ByteRange extends HttpRange {
235
236                private final long firstPos;
237
238                private final Long lastPos;
239
240                public ByteRange(long firstPos, Long lastPos) {
241                        assertPositions(firstPos, lastPos);
242                        this.firstPos = firstPos;
243                        this.lastPos = lastPos;
244                }
245
246                private void assertPositions(long firstBytePos, Long lastBytePos) {
247                        if (firstBytePos < 0) {
248                                throw new IllegalArgumentException("Invalid first byte position: " + firstBytePos);
249                        }
250                        if (lastBytePos != null && lastBytePos < firstBytePos) {
251                                throw new IllegalArgumentException("firstBytePosition=" + firstBytePos +
252                                                " should be less then or equal to lastBytePosition=" + lastBytePos);
253                        }
254                }
255
256                @Override
257                public long getRangeStart(long length) {
258                        return this.firstPos;
259                }
260
261                @Override
262                public long getRangeEnd(long length) {
263                        if (this.lastPos != null && this.lastPos < length) {
264                                return this.lastPos;
265                        }
266                        else {
267                                return length - 1;
268                        }
269                }
270
271                @Override
272                public boolean equals(Object other) {
273                        if (this == other) {
274                                return true;
275                        }
276                        if (!(other instanceof ByteRange)) {
277                                return false;
278                        }
279                        ByteRange otherRange = (ByteRange) other;
280                        return (this.firstPos == otherRange.firstPos &&
281                                        ObjectUtils.nullSafeEquals(this.lastPos, otherRange.lastPos));
282                }
283
284                @Override
285                public int hashCode() {
286                        return (ObjectUtils.nullSafeHashCode(this.firstPos) * 31 +
287                                        ObjectUtils.nullSafeHashCode(this.lastPos));
288                }
289
290                @Override
291                public String toString() {
292                        StringBuilder builder = new StringBuilder();
293                        builder.append(this.firstPos);
294                        builder.append('-');
295                        if (this.lastPos != null) {
296                                builder.append(this.lastPos);
297                        }
298                        return builder.toString();
299                }
300        }
301
302
303        /**
304         * Represents an HTTP/1.1 suffix byte range, with a number of suffix bytes.
305         * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
306         * @see HttpRange#createSuffixRange(long)
307         */
308        private static class SuffixByteRange extends HttpRange {
309
310                private final long suffixLength;
311
312                public SuffixByteRange(long suffixLength) {
313                        if (suffixLength < 0) {
314                                throw new IllegalArgumentException("Invalid suffix length: " + suffixLength);
315                        }
316                        this.suffixLength = suffixLength;
317                }
318
319                @Override
320                public long getRangeStart(long length) {
321                        if (this.suffixLength < length) {
322                                return length - this.suffixLength;
323                        }
324                        else {
325                                return 0;
326                        }
327                }
328
329                @Override
330                public long getRangeEnd(long length) {
331                        return length - 1;
332                }
333
334                @Override
335                public boolean equals(Object other) {
336                        if (this == other) {
337                                return true;
338                        }
339                        if (!(other instanceof SuffixByteRange)) {
340                                return false;
341                        }
342                        SuffixByteRange otherRange = (SuffixByteRange) other;
343                        return (this.suffixLength == otherRange.suffixLength);
344                }
345
346                @Override
347                public int hashCode() {
348                        return ObjectUtils.hashCode(this.suffixLength);
349                }
350
351                @Override
352                public String toString() {
353                        return "-" + this.suffixLength;
354                }
355        }
356
357}