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