Range.java

package com.renomad.minum.web;

import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

final class Range {

    private static final Pattern rangeHeaderPattern = Pattern.compile("^bytes=(?<first>[0-9]{0,13})-(?<second>[0-9]{0,13})$");
    private final Long rangeFirstPart;
    private final Long rangeSecondPart;
    private final Long length;
    private final Long offset;
    private final boolean hasRangeHeader;

    public Range(Headers requestHeaders, long fullLength) {
        List<String> rangeHeaders = requestHeaders.valueByKey("range");
        if (rangeHeaders == null) {
            hasRangeHeader = false;
            rangeFirstPart = null;
            rangeSecondPart = null;
            length = fullLength;
            offset = 0L;
        } else {
            hasRangeHeader = true;
            if (rangeHeaders.size() > 1) {
                throw new InvalidRangeException("Error: Request contained more than one Range header");
            }
            // the "Range:" header provides a desired range, and can request multiple ranges.
            // this server does not currently handle multiple ranges, so if that is requested we
            // will ignore the range header and return a 200 with the entire contents.
            Matcher matcher = rangeHeaderPattern.matcher(rangeHeaders.getFirst());
            if (matcher.matches()) {
                String firstPart = matcher.group("first");
                String secondPart = matcher.group("second");

                if (!firstPart.isEmpty()) {
                    rangeFirstPart = Long.parseLong(firstPart);
                } else {
                    rangeFirstPart = null;
                }

                if (!secondPart.isEmpty()) {
                    rangeSecondPart = Long.parseLong(secondPart);
                } else {
                    rangeSecondPart = null;
                }

                // options
                // 1: there's a first and second part
                // 2: only a second part
                // 3: only a first part
                // 4: (invalid) the first part is larger than the second part
                // 5: (invalid) either of the range values are invalid longs
                if (rangeFirstPart != null && rangeSecondPart != null) {
                    if (rangeFirstPart > rangeSecondPart) {
                        throw new InvalidRangeException("Error: The value of the first part of the range was larger than the second.");
                    } else {
                        length = (rangeSecondPart - rangeFirstPart) + 1;
                        offset = rangeFirstPart;
                    }
                } else if (rangeFirstPart != null) {
                    offset = rangeFirstPart;
                    length = fullLength - offset;
                } else if (rangeSecondPart != null) {
                    offset = fullLength - rangeSecondPart;
                    length = rangeSecondPart;
                } else {
                    length = fullLength;
                    offset = 0L;
                }

            } else {
                rangeFirstPart = null;
                rangeSecondPart = null;
                length = fullLength;
                offset = 0L;
            }
        }
    }

    public Long getRangeFirstPart() {
        return rangeFirstPart;
    }

    public Long getRangeSecondPart() {
        return rangeSecondPart;
    }

    public Long getLength() {
        return length;
    }

    public Long getOffset() {
        return offset;
    }

    public boolean hasRangeHeader() {
        return hasRangeHeader;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Range range = (Range) o;
        return hasRangeHeader == range.hasRangeHeader && Objects.equals(rangeFirstPart, range.rangeFirstPart) && Objects.equals(rangeSecondPart, range.rangeSecondPart) && Objects.equals(length, range.length) && Objects.equals(offset, range.offset);
    }

    @Override
    public int hashCode() {
        return Objects.hash(rangeFirstPart, rangeSecondPart, length, offset, hasRangeHeader);
    }

    @Override
    public String toString() {
        return "Range{" +
                "rangeFirstPart=" + rangeFirstPart +
                ", rangeSecondPart=" + rangeSecondPart +
                ", length=" + length +
                ", offset=" + offset +
                ", hasRangeHeader=" + hasRangeHeader +
                '}';
    }
}