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}