HttpRange.java 11.2 KB
Newer Older
1
/*
S
Sam Brannen 已提交
2
 * Copyright 2002-2019 the original author or authors.
3 4 5 6 7
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
S
Spring Operator 已提交
8
 *      https://www.apache.org/licenses/LICENSE-2.0
9 10 11 12 13 14 15 16 17 18
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.http;

19
import java.io.IOException;
20 21 22 23
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
24
import java.util.StringJoiner;
25

26 27
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
28
import org.springframework.core.io.support.ResourceRegion;
29
import org.springframework.lang.Nullable;
30
import org.springframework.util.Assert;
J
Juergen Hoeller 已提交
31
import org.springframework.util.CollectionUtils;
32 33 34 35
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
36
 * Represents an HTTP (byte) range for use with the HTTP {@code "Range"} header.
37 38
 *
 * @author Arjen Poutsma
J
Juergen Hoeller 已提交
39 40
 * @author Juergen Hoeller
 * @since 4.2
S
Spring Operator 已提交
41
 * @see <a href="https://tools.ietf.org/html/rfc7233">HTTP/1.1: Range Requests</a>
42 43 44 45 46
 * @see HttpHeaders#setRange(List)
 * @see HttpHeaders#getRange()
 */
public abstract class HttpRange {

47 48 49
	/** Maximum ranges per request. */
	private static final int MAX_RANGES = 100;

50 51 52
	private static final String BYTE_RANGE_PREFIX = "bytes=";


53 54 55 56 57
	/**
	 * Turn a {@code Resource} into a {@link ResourceRegion} using the range
	 * information contained in the current {@code HttpRange}.
	 * @param resource the {@code Resource} to select the region from
	 * @return the selected region of the given {@code Resource}
58
	 * @since 4.3
59 60 61 62
	 */
	public ResourceRegion toResourceRegion(Resource resource) {
		// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
		// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
J
Juergen Hoeller 已提交
63 64
		Assert.isTrue(resource.getClass() != InputStreamResource.class,
				"Cannot convert an InputStreamResource to a ResourceRegion");
65 66 67
		long contentLength = getLengthFor(resource);
		long start = getRangeStart(contentLength);
		long end = getRangeEnd(contentLength);
68
		Assert.isTrue(start < contentLength, "'position' exceeds the resource length " + contentLength);
69 70 71
		return new ResourceRegion(resource, start, end - start + 1);
	}

J
Juergen Hoeller 已提交
72 73 74 75 76 77 78 79 80 81 82 83 84 85
	/**
	 * Return the start of the range given the total length of a representation.
	 * @param length the length of the representation
	 * @return the start of this range for the representation
	 */
	public abstract long getRangeStart(long length);

	/**
	 * Return the end of the range (inclusive) given the total length of a representation.
	 * @param length the length of the representation
	 * @return the end of the range for the representation
	 */
	public abstract long getRangeEnd(long length);

86 87 88

	/**
	 * Create an {@code HttpRange} from the given position to the end.
89
	 * @param firstBytePos the first byte position
90
	 * @return a byte range that ranges from {@code firstPos} till the end
S
Spring Operator 已提交
91
	 * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
92 93 94 95 96 97
	 */
	public static HttpRange createByteRange(long firstBytePos) {
		return new ByteRange(firstBytePos, null);
	}

	/**
98
	 * Create a {@code HttpRange} from the given fist to last position.
99 100
	 * @param firstBytePos the first byte position
	 * @param lastBytePos the last byte position
101
	 * @return a byte range that ranges from {@code firstPos} till {@code lastPos}
S
Spring Operator 已提交
102
	 * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
103 104 105 106 107 108
	 */
	public static HttpRange createByteRange(long firstBytePos, long lastBytePos) {
		return new ByteRange(firstBytePos, lastBytePos);
	}

	/**
109 110
	 * Create an {@code HttpRange} that ranges over the last given number of bytes.
	 * @param suffixLength the number of bytes for the range
111
	 * @return a byte range that ranges over the last {@code suffixLength} number of bytes
S
Spring Operator 已提交
112
	 * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
113 114 115 116 117 118 119 120 121 122
	 */
	public static HttpRange createSuffixRange(long suffixLength) {
		return new SuffixByteRange(suffixLength);
	}

	/**
	 * Parse the given, comma-separated string into a list of {@code HttpRange} objects.
	 * <p>This method can be used to parse an {@code Range} header.
	 * @param ranges the string to parse
	 * @return the list of ranges
123 124
	 * @throws IllegalArgumentException if the string cannot be parsed
	 * or if the number of ranges is greater than 100
125
	 */
126
	public static List<HttpRange> parseRanges(@Nullable String ranges) {
127 128 129 130
		if (!StringUtils.hasLength(ranges)) {
			return Collections.emptyList();
		}
		if (!ranges.startsWith(BYTE_RANGE_PREFIX)) {
131
			throw new IllegalArgumentException("Range '" + ranges + "' does not start with 'bytes='");
132 133 134
		}
		ranges = ranges.substring(BYTE_RANGE_PREFIX.length());

135
		String[] tokens = StringUtils.tokenizeToStringArray(ranges, ",");
136 137 138
		if (tokens.length > MAX_RANGES) {
			throw new IllegalArgumentException("Too many ranges: " + tokens.length);
		}
139
		List<HttpRange> result = new ArrayList<>(tokens.length);
140 141 142 143 144 145 146
		for (String token : tokens) {
			result.add(parseRange(token));
		}
		return result;
	}

	private static HttpRange parseRange(String range) {
J
Juergen Hoeller 已提交
147
		Assert.hasLength(range, "Range String must not be empty");
148
		int dashIdx = range.indexOf('-');
149
		if (dashIdx > 0) {
150 151
			long firstPos = Long.parseLong(range.substring(0, dashIdx));
			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 161 162
			long suffixLength = Long.parseLong(range.substring(1));
			return new SuffixByteRange(suffixLength);
		}
163 164 165
		else {
			throw new IllegalArgumentException("Range '" + range + "' does not contain \"-\"");
		}
166 167
	}

168
	/**
J
Juergen Hoeller 已提交
169 170
	 * Convert each {@code HttpRange} into a {@code ResourceRegion}, selecting the
	 * appropriate segment of the given {@code Resource} using HTTP Range information.
171 172 173
	 * @param ranges the list of ranges
	 * @param resource the resource to select the regions from
	 * @return the list of regions for the given resource
174
	 * @throws IllegalArgumentException if the sum of all ranges exceeds the resource length
J
Juergen Hoeller 已提交
175
	 * @since 4.3
176 177
	 */
	public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) {
J
Juergen Hoeller 已提交
178
		if (CollectionUtils.isEmpty(ranges)) {
179 180
			return Collections.emptyList();
		}
181
		List<ResourceRegion> regions = new ArrayList<>(ranges.size());
J
Juergen Hoeller 已提交
182
		for (HttpRange range : ranges) {
183 184
			regions.add(range.toResourceRegion(resource));
		}
185 186
		if (ranges.size() > 1) {
			long length = getLengthFor(resource);
187 188 189 190
			long total = 0;
			for (ResourceRegion region : regions) {
				total += region.getCount();
			}
191 192 193 194
			if (total >= length) {
				throw new IllegalArgumentException("The sum of all ranges (" + total +
						") should be less than the resource length (" + length + ")");
			}
195
		}
196 197 198
		return regions;
	}

199 200 201 202 203 204 205 206 207 208 209
	private static long getLengthFor(Resource resource) {
		try {
			long contentLength = resource.contentLength();
			Assert.isTrue(contentLength > 0, "Resource content length should be > 0");
			return contentLength;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Failed to obtain Resource content length", ex);
		}
	}

210 211 212 213 214 215 216
	/**
	 * Return a string representation of the given list of {@code HttpRange} objects.
	 * <p>This method can be used to for an {@code Range} header.
	 * @param ranges the ranges to create a string of
	 * @return the string representation
	 */
	public static String toString(Collection<HttpRange> ranges) {
J
Juergen Hoeller 已提交
217
		Assert.notEmpty(ranges, "Ranges Collection must not be empty");
218 219 220
		StringJoiner builder = new StringJoiner(", ", BYTE_RANGE_PREFIX, "");
		for (HttpRange range : ranges) {
			builder.add(range.toString());
221 222 223 224
		}
		return builder.toString();
	}

225

226 227
	/**
	 * Represents an HTTP/1.1 byte range, with a first and optional last position.
S
Spring Operator 已提交
228
	 * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
229 230 231 232 233 234 235
	 * @see HttpRange#createByteRange(long)
	 * @see HttpRange#createByteRange(long, long)
	 */
	private static class ByteRange extends HttpRange {

		private final long firstPos;

236
		@Nullable
237 238
		private final Long lastPos;

239
		public ByteRange(long firstPos, @Nullable Long lastPos) {
240
			assertPositions(firstPos, lastPos);
241 242 243 244
			this.firstPos = firstPos;
			this.lastPos = lastPos;
		}

245
		private void assertPositions(long firstBytePos, @Nullable Long lastBytePos) {
246
			if (firstBytePos < 0) {
J
Juergen Hoeller 已提交
247
				throw new IllegalArgumentException("Invalid first byte position: " + firstBytePos);
248 249
			}
			if (lastBytePos != null && lastBytePos < firstBytePos) {
J
Juergen Hoeller 已提交
250
				throw new IllegalArgumentException("firstBytePosition=" + firstBytePos +
251 252 253 254
						" should be less then or equal to lastBytePosition=" + lastBytePos);
			}
		}

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
		@Override
		public long getRangeStart(long length) {
			return this.firstPos;
		}

		@Override
		public long getRangeEnd(long length) {
			if (this.lastPos != null && this.lastPos < length) {
				return this.lastPos;
			}
			else {
				return length - 1;
			}
		}

		@Override
271
		public boolean equals(@Nullable Object other) {
J
Juergen Hoeller 已提交
272
			if (this == other) {
273 274
				return true;
			}
J
Juergen Hoeller 已提交
275
			if (!(other instanceof ByteRange)) {
276 277
				return false;
			}
J
Juergen Hoeller 已提交
278 279 280
			ByteRange otherRange = (ByteRange) other;
			return (this.firstPos == otherRange.firstPos &&
					ObjectUtils.nullSafeEquals(this.lastPos, otherRange.lastPos));
281 282 283 284
		}

		@Override
		public int hashCode() {
J
Juergen Hoeller 已提交
285 286 287 288 289 290 291 292 293 294 295 296 297
			return (ObjectUtils.nullSafeHashCode(this.firstPos) * 31 +
					ObjectUtils.nullSafeHashCode(this.lastPos));
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append(this.firstPos);
			builder.append('-');
			if (this.lastPos != null) {
				builder.append(this.lastPos);
			}
			return builder.toString();
298 299 300
		}
	}

J
Juergen Hoeller 已提交
301

302 303
	/**
	 * Represents an HTTP/1.1 suffix byte range, with a number of suffix bytes.
S
Spring Operator 已提交
304
	 * @see <a href="https://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
305 306 307 308 309 310
	 * @see HttpRange#createSuffixRange(long)
	 */
	private static class SuffixByteRange extends HttpRange {

		private final long suffixLength;

J
Juergen Hoeller 已提交
311
		public SuffixByteRange(long suffixLength) {
312
			if (suffixLength < 0) {
J
Juergen Hoeller 已提交
313
				throw new IllegalArgumentException("Invalid suffix length: " + suffixLength);
314
			}
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
			this.suffixLength = suffixLength;
		}

		@Override
		public long getRangeStart(long length) {
			if (this.suffixLength < length) {
				return length - this.suffixLength;
			}
			else {
				return 0;
			}
		}

		@Override
		public long getRangeEnd(long length) {
			return length - 1;
		}

333
		@Override
334
		public boolean equals(@Nullable Object other) {
J
Juergen Hoeller 已提交
335
			if (this == other) {
336 337
				return true;
			}
J
Juergen Hoeller 已提交
338
			if (!(other instanceof SuffixByteRange)) {
339 340
				return false;
			}
J
Juergen Hoeller 已提交
341 342
			SuffixByteRange otherRange = (SuffixByteRange) other;
			return (this.suffixLength == otherRange.suffixLength);
343 344 345 346
		}

		@Override
		public int hashCode() {
347
			return Long.hashCode(this.suffixLength);
348
		}
J
Juergen Hoeller 已提交
349 350 351 352 353

		@Override
		public String toString() {
			return "-" + this.suffixLength;
		}
354
	}
J
Juergen Hoeller 已提交
355

356
}