/*
* The MIT License
*
* Copyright 2015 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.mastfrog.acteur.headers;
import com.mastfrog.acteur.headers.Range;
import com.mastfrog.util.collections.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Implementation of byte-range headers as described in
* <a href="http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#range.units">this
* spec</a>.
*
* @author Tim Boudreau
*/
public final class ByteRanges implements Iterable<Range> {
private final boolean valid;
public static final Pattern RANGE_PATTERN = Pattern.compile("^(\\d+)-(\\d+)");
public static final Pattern START_RANGE_PATTERN = Pattern.compile("^(\\d+)-");
public static final Pattern END_RANGE_PATTERN = Pattern.compile("^-(\\d+)");
private final Range[] ranges;
private ByteRanges(List<Range> ranges) {
this.valid = true;
this.ranges = ranges.toArray(new Range[ranges.size()]);
}
public ByteRanges(String rangeHeader) {
boolean valid = true;
List<Range> items = new ArrayList<>(4);
if (!rangeHeader.startsWith("bytes=")) {
valid = false;
} else {
String rangeInfo = rangeHeader.substring("bytes=".length()).trim();
if (rangeInfo.length() == 0) {
valid = false;
} else {
String[] ranges = rangeInfo.split(",");
for (String range : ranges) {
range = range.trim();
Matcher m = RANGE_PATTERN.matcher(range);
if (m.find()) {
try {
long start = Long.parseLong(m.group(1));
long end = Long.parseLong(m.group(2));
if (start < 0 || end < 0) {
valid = false;
break;
}
if (end <= start) {
valid = false;
break;
}
items.add(new RangeImpl(start, end));
} catch (NumberFormatException nfe) {
valid = false;
break;
}
} else {
m = START_RANGE_PATTERN.matcher(range);
if (m.find()) {
try {
long val = Long.parseLong(m.group(1));
if (val == -1) {
valid = false;
break;
}
items.add(new StartRange(val));
} catch (NumberFormatException e) {
valid = false;
break;
}
} else {
m = END_RANGE_PATTERN.matcher(range);
if (m.find()) {
try {
long val = Long.parseLong(m.group(1));
if (val < 0) {
valid = false;
break;
}
items.add(new EndRange(val));
} catch (NumberFormatException nfe) {
valid = false;
break;
}
}
}
}
}
}
}
ranges = items.toArray(new Range[items.size()]);
this.valid = valid;
}
public int size() {
return ranges.length;
}
public Range first() {
return ranges.length > 0 ? ranges[0] : null;
}
public Range get(int which) {
return ranges[which];
}
public boolean isValid() {
return valid;
}
@Override
public Iterator<Range> iterator() {
return CollectionUtils.toIterator(ranges);
}
public String toString() {
StringBuilder result = new StringBuilder("bytes=");
for (int i = 0; i < ranges.length; i++) {
result.append(ranges[i]);
if (i < ranges.length - 1) {
result.append(',');
}
}
return result.toString();
}
public boolean equals(Object o) {
return o instanceof ByteRanges && o.toString().equals(toString());
}
public int hashCode() {
return valid ? 1 : -1 + (7 * Arrays.hashCode(ranges));
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private final List<Range> ranges = new ArrayList<>(4);
private Builder() {
}
public final ByteRanges build() {
return new ByteRanges(ranges);
}
public Builder add(Range range) {
this.ranges.add(range);
return this;
}
public Builder add(long start, long end) {
if (end <= start) {
throw new IllegalArgumentException("start=" + start + ", end=" + end);
}
ranges.add(new RangeImpl(start, end));
return this;
}
public Builder addStartpoint(long start) {
if (start < 0) {
throw new IllegalArgumentException("Negative start " + start);
}
ranges.add(new StartRange(start));
return this;
}
public Builder addFromEnd(long subtract) {
if (subtract < 0) {
throw new IllegalArgumentException("Negative subtract: " + subtract);
}
ranges.add(new EndRange(subtract));
return this;
}
}
private static final class RangeImpl implements Range {
final long start;
final long end;
public RangeImpl(long start, long end) {
this.start = start;
this.end = end;
}
@Override
public long start(long max) {
if (start > max) {
return -1;
}
return start;
}
@Override
public long end(long max) {
if (end > max) {
return -1;
}
return end;
}
public String toString() {
return start + "-" + end;
}
@Override
public BoundedRange toBoundedRange(long max) {
return new BoundedRange(start(max), end(max), max);
}
}
private static final class EndRange implements Range {
final long endOffset;
public EndRange(long endOffset) {
this.endOffset = endOffset;
}
@Override
public long start(long max) {
long result = max - endOffset;
if (result < 0) {
return -1;
}
return result;
}
@Override
public long end(long max) {
if (endOffset > max) {
return -1;
}
return max;
}
public String toString() {
return "-" + endOffset;
}
@Override
public BoundedRange toBoundedRange(long max) {
return new BoundedRange(start(max), end(max), max);
}
}
public static final class StartRange implements Range {
final long startpoint;
public StartRange(long startpoint) {
this.startpoint = startpoint;
}
@Override
public long start(long max) {
if (startpoint > max) {
return -1;
}
return startpoint;
}
@Override
public long end(long max) {
return max;
}
public String toString() {
return startpoint + "-";
}
@Override
public BoundedRange toBoundedRange(long max) {
return new BoundedRange(start(max), end(max), max);
}
}
}