/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 io.undertow.util;
import io.undertow.UndertowLogger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
Represents a byte range for a range request
Author: Stuart Douglas
/**
* Represents a byte range for a range request
*
*
* @author Stuart Douglas
*/
public class ByteRange {
private final List<Range> ranges;
public ByteRange(List<Range> ranges) {
this.ranges = ranges;
}
public int getRanges() {
return ranges.size();
}
Gets the start of the specified range segment, of -1 if this is a suffix range segment
Params: - range – The range segment to get
Returns: The range start
/**
* Gets the start of the specified range segment, of -1 if this is a suffix range segment
* @param range The range segment to get
* @return The range start
*/
public long getStart(int range) {
return ranges.get(range).getStart();
}
Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment
Params: - range – The range segment to get
Returns: The range end
/**
* Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment
* @param range The range segment to get
* @return The range end
*/
public long getEnd(int range) {
return ranges.get(range).getEnd();
}
Attempts to parse a range request. If the range request is invalid it will just return null so that
it may be ignored.
Params: - rangeHeader – The range spec
Returns: A range spec, or null if the range header could not be parsed
/**
* Attempts to parse a range request. If the range request is invalid it will just return null so that
* it may be ignored.
*
*
* @param rangeHeader The range spec
* @return A range spec, or null if the range header could not be parsed
*/
public static ByteRange parse(String rangeHeader) {
if(rangeHeader == null || rangeHeader.length() < 7) {
return null;
}
if(!rangeHeader.startsWith("bytes=")) {
return null;
}
List<Range> ranges = new ArrayList<>();
String[] parts = rangeHeader.substring(6).split(",");
for(String part : parts) {
try {
int index = part.indexOf('-');
if (index == 0) {
//suffix range spec
//represents the last N bytes
//internally we represent this using a -1 as the start position
long val = Long.parseLong(part.substring(1));
if(val < 0) {
UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
return null;
}
ranges.add(new Range(-1, val));
} else {
if(index == -1) {
UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
return null;
}
long start = Long.parseLong(part.substring(0, index));
if(start < 0) {
UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
return null;
}
long end;
if (index + 1 < part.length()) {
end = Long.parseLong(part.substring(index + 1));
} else {
end = -1;
}
ranges.add(new Range(start, end));
}
} catch (NumberFormatException e) {
UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
return null;
}
}
if(ranges.isEmpty()) {
return null;
}
return new ByteRange(ranges);
}
Returns a representation of the range result. If this returns null then a 200 response should be sent instead
Params: - resourceContentLength –
Returns:
/**
* Returns a representation of the range result. If this returns null then a 200 response should be sent instead
* @param resourceContentLength
* @return
*/
public RangeResponseResult getResponseResult(final long resourceContentLength, String ifRange, Date lastModified, String eTag) {
if(ranges.isEmpty()) {
return null;
}
long start = getStart(0);
long end = getEnd(0);
long rangeLength;
if(ifRange != null && !ifRange.isEmpty()) {
if(ifRange.charAt(0) == '"') {
//entity tag
if(eTag != null && !eTag.equals(ifRange)) {
return null;
}
} else {
Date ifDate = DateUtils.parseDate(ifRange);
if(ifDate != null && lastModified != null && ifDate.getTime() < lastModified.getTime()) {
return null;
}
}
}
if(start == -1 ) {
//suffix range
if(end < 0){
//ignore the range request
return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE);
}
start = Math.max(resourceContentLength - end, 0);
end = resourceContentLength - 1;
rangeLength = resourceContentLength - start;
} else if(end == -1) {
//prefix range
long toWrite = resourceContentLength - start;
if (toWrite > 0) {
rangeLength = toWrite;
} else {
//ignore the range request
return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE);
}
end = resourceContentLength - 1;
} else {
end = Math.min(end, resourceContentLength - 1);
if(start >= resourceContentLength || start > end) {
return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE);
}
rangeLength = end - start + 1;
}
return new RangeResponseResult(start, end, rangeLength, "bytes " + start + "-" + end + "/" + resourceContentLength, StatusCodes.PARTIAL_CONTENT);
}
public static class RangeResponseResult {
private final long start;
private final long end;
private final long contentLength;
private final String contentRange;
private final int statusCode;
public RangeResponseResult(long start, long end, long contentLength, String contentRange, int statusCode) {
this.start = start;
this.end = end;
this.contentLength = contentLength;
this.contentRange = contentRange;
this.statusCode = statusCode;
}
public long getStart() {
return start;
}
public long getEnd() {
return end;
}
public long getContentLength() {
return contentLength;
}
public String getContentRange() {
return contentRange;
}
public int getStatusCode() {
return statusCode;
}
}
public static class Range {
private final long start, end;
public Range(long start, long end) {
this.start = start;
this.end = end;
}
public long getStart() {
return start;
}
public long getEnd() {
return end;
}
}
}