Range.java
/*
* *********************************************************************************************************************
*
* blueMarine II: Semantic Media Centre
* http://tidalwave.it/projects/bluemarine2
*
* Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
*
* *********************************************************************************************************************
*
* 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.
*
* *********************************************************************************************************************
*
* git clone https://bitbucket.org/tidalwave/bluemarine2-src
* git clone https://github.com/tidalwave-it/bluemarine2-src
*
* *********************************************************************************************************************
*/
package it.tidalwave.bluemarine2.rest.impl;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/***********************************************************************************************************************
*
* @author Fabrizio Giudici
*
**********************************************************************************************************************/
@Slf4j @Getter @EqualsAndHashCode
public class Range
{
private final long start;
private final long end;
private final long length;
private final long total;
/*******************************************************************************************************************
*
* Construct a byte range.
*
* @param start start of the byte range.
* @param end end of the byte range.
* @param total total length of the byte source.
*
******************************************************************************************************************/
public Range (@Nonnegative final long start, @Nonnegative final long end, @Nonnegative final long total)
{
this.start = start;
this.end = end;
this.length = end - start + 1;
this.total = total;
}
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
@Nonnull
public static Range full (@Nonnegative final long total)
{
return new Range(0, total - 1, total);
}
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
@Nonnull
public Range subrange (@Nonnegative final long size)
{
return new Range(start, Math.min(end, start + size - 1), total);
}
/*******************************************************************************************************************
*
* Parses a range from a HTTP header.
*
* @param rangeHeader the HTTP header
* @param total the length of the full datum
* @return the range
*
******************************************************************************************************************/
@Nonnull
public static List<Range> fromHeader (@Nullable final String rangeHeader, @Nonnegative final long total)
{
final List<Range> ranges = new ArrayList<>();
if (rangeHeader != null)
{
// Range header should match format "bytes=n-n,n-n,n-n...".
if (!rangeHeader.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$"))
{
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
// If any valid If-Range header, then process each part of byte range.
for (final String part : rangeHeader.substring(6).split(","))
{
// Assuming a file with length of 100, the following examples returns bytes at:
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
long start = subStringOrMinusOne(part, 0, part.indexOf("-"));
long end = subStringOrMinusOne(part, part.indexOf("-") + 1, part.length());
if (start == -1)
{
start = total - end;
end = total - 1;
}
else if ((end == -1) || (end > total - 1))
{
end = total - 1;
}
if (start > end)
{
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
ranges.add(new Range(start, end, total));
}
}
return ranges;
}
/*******************************************************************************************************************
*
* Returns a {@link ResourceRegion} mapping the portion of bytes matching this range.
*
* @param resource the resource
* @return the region
*
******************************************************************************************************************/
@Nonnull
public ResourceRegion getRegion (@Nonnull final Resource resource)
{
return new ResourceRegion(resource, start, length);
}
/*******************************************************************************************************************
*
* Returns a substring of the given string value from the given begin index to the given end index as a long. If the
* substring is empty, then -1 will be returned
*
* @param value the string value to return a substring as long for.
* @param beginIndex the begin index of the substring to be returned as long.
* @param endIndex the end index of the substring to be returned as long.
* @return a substring of the given string value as long or -1 if substring is empty.
*
******************************************************************************************************************/
private static long subStringOrMinusOne (@Nonnull final String value,
@Nonnegative final int beginIndex,
@Nonnegative final int endIndex)
{
final String substring = value.substring(beginIndex, endIndex);
return (substring.length() > 0) ? Long.parseLong(substring) : -1;
}
}