Range.java

  1. /*
  2.  * *********************************************************************************************************************
  3.  *
  4.  * blueMarine II: Semantic Media Centre
  5.  * http://tidalwave.it/projects/bluemarine2
  6.  *
  7.  * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
  8.  *
  9.  * *********************************************************************************************************************
  10.  *
  11.  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  12.  * the License. You may obtain a copy of the License at
  13.  *
  14.  *     http://www.apache.org/licenses/LICENSE-2.0
  15.  *
  16.  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  17.  * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
  18.  * specific language governing permissions and limitations under the License.
  19.  *
  20.  * *********************************************************************************************************************
  21.  *
  22.  * git clone https://bitbucket.org/tidalwave/bluemarine2-src
  23.  * git clone https://github.com/tidalwave-it/bluemarine2-src
  24.  *
  25.  * *********************************************************************************************************************
  26.  */
  27. package it.tidalwave.bluemarine2.rest.impl;

  28. import javax.annotation.Nonnegative;
  29. import javax.annotation.Nonnull;
  30. import javax.annotation.Nullable;
  31. import java.util.ArrayList;
  32. import java.util.List;
  33. import org.springframework.core.io.Resource;
  34. import org.springframework.core.io.support.ResourceRegion;
  35. import lombok.EqualsAndHashCode;
  36. import lombok.Getter;
  37. import lombok.extern.slf4j.Slf4j;

  38. /***********************************************************************************************************************
  39.  *
  40.  * @author  Fabrizio Giudici
  41.  *
  42.  **********************************************************************************************************************/
  43. @Slf4j @Getter @EqualsAndHashCode
  44. public class Range
  45.   {
  46.     private final long start;
  47.     private final long end;
  48.     private final long length;
  49.     private final long total;

  50.     /*******************************************************************************************************************
  51.      *
  52.      * Construct a byte range.
  53.      *
  54.      * @param   start   start of the byte range.
  55.      * @param   end     end of the byte range.
  56.      * @param   total   total length of the byte source.
  57.      *
  58.      ******************************************************************************************************************/
  59.     public Range (@Nonnegative final long start, @Nonnegative final long end, @Nonnegative final long total)
  60.       {
  61.         this.start = start;
  62.         this.end = end;
  63.         this.length = end - start + 1;
  64.         this.total = total;
  65.       }

  66.     /*******************************************************************************************************************
  67.      *
  68.      *
  69.      ******************************************************************************************************************/
  70.     @Nonnull
  71.     public static Range full (@Nonnegative final long total)
  72.       {
  73.         return new Range(0, total - 1, total);
  74.       }

  75.     /*******************************************************************************************************************
  76.      *
  77.      *
  78.      ******************************************************************************************************************/
  79.     @Nonnull
  80.     public Range subrange (@Nonnegative final long size)
  81.       {
  82.        return new Range(start, Math.min(end, start + size - 1), total);
  83.       }

  84.     /*******************************************************************************************************************
  85.      *
  86.      * Parses a range from a HTTP header.
  87.      *
  88.      * @param   rangeHeader     the HTTP header
  89.      * @param   total           the length of the full datum
  90.      * @return                  the range
  91.      *
  92.      ******************************************************************************************************************/
  93.     @Nonnull
  94.     public static List<Range> fromHeader (@Nullable final String rangeHeader, @Nonnegative final long total)
  95.       {
  96.         final List<Range> ranges = new ArrayList<>();

  97.         if (rangeHeader != null)
  98.           {
  99.             // Range header should match format "bytes=n-n,n-n,n-n...".
  100.             if (!rangeHeader.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$"))
  101.               {
  102.                 throw new IllegalArgumentException("Invalid range: " + rangeHeader);
  103.               }

  104.             // If any valid If-Range header, then process each part of byte range.
  105.             for (final String part : rangeHeader.substring(6).split(","))
  106.               {
  107.                 // Assuming a file with length of 100, the following examples returns bytes at:
  108.                 // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
  109.                 long start = subStringOrMinusOne(part, 0, part.indexOf("-"));
  110.                 long end = subStringOrMinusOne(part, part.indexOf("-") + 1, part.length());

  111.                 if (start == -1)
  112.                   {
  113.                     start = total - end;
  114.                     end = total - 1;
  115.                   }
  116.                 else if ((end == -1) || (end > total - 1))
  117.                   {
  118.                     end = total - 1;
  119.                   }

  120.                 if (start > end)
  121.                   {
  122.                     throw new IllegalArgumentException("Invalid range: " + rangeHeader);
  123.                   }

  124.                 ranges.add(new Range(start, end, total));
  125.               }
  126.           }

  127.         return ranges;
  128.       }

  129.     /*******************************************************************************************************************
  130.      *
  131.      * Returns a {@link ResourceRegion} mapping the portion of bytes matching this range.
  132.      *
  133.      * @param       resource    the resource
  134.      * @return                  the region
  135.      *
  136.      ******************************************************************************************************************/
  137.     @Nonnull
  138.     public ResourceRegion getRegion (@Nonnull final Resource resource)
  139.       {
  140.         return new ResourceRegion(resource, start, length);
  141.       }

  142.     /*******************************************************************************************************************
  143.      *
  144.      * Returns a substring of the given string value from the given begin index to the given end index as a long. If the
  145.      * substring is empty, then -1 will be returned
  146.      *
  147.      * @param   value       the string value to return a substring as long for.
  148.      * @param   beginIndex  the begin index of the substring to be returned as long.
  149.      * @param   endIndex    the end index of the substring to be returned as long.
  150.      * @return              a substring of the given string value as long or -1 if substring is empty.
  151.      *
  152.      ******************************************************************************************************************/
  153.     private static long subStringOrMinusOne (@Nonnull final String value,
  154.                                              @Nonnegative final int beginIndex,
  155.                                              @Nonnegative final int endIndex)
  156.       {
  157.         final String substring = value.substring(beginIndex, endIndex);
  158.         return (substring.length() > 0) ? Long.parseLong(substring) : -1;
  159.       }
  160.   }