blob: e224a9c5d39e9f02c56c86212d70d895a5b4ed1e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.sidecar.models;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.constraints.NotNull;
import com.google.common.base.Preconditions;
import org.apache.cassandra.sidecar.exceptions.RangeException;
/**
* Accepted Range formats are start-end, start-, -suffix_length
* start-end (start = start index of the range, end = end index of the range, both inclusive)
* start- (start = start index of the range, end = end of file)
* -suffix-length (Requested length from end of file. The length should be positive)
*/
public class Range
{
private static final String RANGE_UNIT = "bytes";
// matches a. bytes=1-2, b. bytes=1-, c. bytes=-2, d. bytes=-. Need to do another valid for case d.
private static final Pattern RANGE_HEADER = Pattern.compile("^" + RANGE_UNIT + "=(\\d*)-(\\d*)$");
private final long start;
private final long end;
private final long length;
private static final long BOUND_ABSENT = -1L;
/**
* Accepted RangeHeader formats are bytes=start-end, bytes=start-, bytes=-suffix_length
*/
public static Range parseHeader(final String header, final long fileSize)
{
if (header == null)
{
return new Range(0, fileSize - 1);
}
return Range.parse(header, fileSize);
}
public static Range of(final long start, final long end)
{
return new Range(start, end);
}
/**
* Accepted string formats "bytes=1453-3563", "bytes=-22344", "bytes=5346-"
* Sample invalid string formats "bytes=8-3", "bytes=-", "bytes=-0", "bytes=a-b"
*
* @param fileSize - passed in to convert partial range into absolute range
*/
private static Range parse(@NotNull String rangeHeader, final long fileSize)
{
Matcher m = RANGE_HEADER.matcher(rangeHeader);
if (!m.matches())
{
throw invalidRangeHeaderException(rangeHeader);
}
long left = parseLong(m.group(1), rangeHeader);
long right = parseLong(m.group(2), rangeHeader);
if (left == BOUND_ABSENT && right == BOUND_ABSENT) // matching "bytes=-"
{
throw invalidRangeHeaderException(rangeHeader);
}
else if (left == BOUND_ABSENT) // matching "bytes=-1"
{
long len = Math.min(right, fileSize); // correct the range if it exceeds file size.
return new Range(fileSize - len, fileSize - 1);
}
else if (right == BOUND_ABSENT) // matching "bytes=1-"
{
return new Range(left, fileSize - 1);
}
else
{
return new Range(left, right);
}
}
// return -1 for empty string; return long value otherwise.
// throws IllegalArgumentException for invalid value string
private static long parseLong(String valStr, String rangeHeader)
{
if (valStr == null || valStr.isEmpty())
return BOUND_ABSENT;
try
{
return Long.parseLong(valStr);
}
catch (NumberFormatException e)
{
throw invalidRangeHeaderException(rangeHeader);
}
}
private static IllegalArgumentException invalidRangeHeaderException(String rangeHeader)
{
return new IllegalArgumentException("Invalid range header: " + rangeHeader + ". " +
"Supported Range formats are bytes=<start>-<end>, bytes=<start>-, " +
"bytes=-<suffix-length>");
}
// An initialized range is always valid; invalid params fail range initialization.
private Range(final long start, final long end)
{
Preconditions.checkArgument(start >= 0, "Range start can not be negative");
Preconditions.checkArgument(end >= start, "Range does not satisfy boundary requirements");
this.start = start;
this.end = end;
long len = end - start + 1; // Assign long max if overflows
this.length = len < 0 ? Long.MAX_VALUE : len;
}
public long start()
{
return this.start;
}
public long end()
{
return this.end;
}
public long length()
{
return this.length;
}
public Range intersect(@NotNull final Range range)
{
long newStart = Math.max(start, range.start());
long newEnd = Math.min(end, range.end());
if (newStart > newEnd)
{
throw new RangeException("Range does not overlap");
}
return new Range(newStart, newEnd);
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof Range))
{
return false;
}
Range range = (Range) o;
return start == range.start &&
end == range.end &&
length == range.length;
}
@Override
public int hashCode()
{
return Objects.hash(start, end, length);
}
@Override
public String toString()
{
return String.format("%s=%d-%d", RANGE_UNIT, start, end);
}
}