/* * Copyright 2016 LinkedIn Corp. All rights reserved. * * 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. */ package com.github.ambry.router; /** * Represents a byte range for performing ranged get requests. */ public class ByteRange { private static final long UNDEFINED_OFFSET = -1; private final ByteRangeType type; private final long startOffset; private final long endOffset; /** * Construct a range from a start offset to an end offset. * @param startOffset the (inclusive) start byte offset. * @param endOffset the (inclusive) end byte offset. * @return A {@link ByteRange} with the specified offsets. * @throws IllegalArgumentException */ public static ByteRange fromOffsetRange(long startOffset, long endOffset) { if (startOffset < 0 || endOffset < startOffset) { throw new IllegalArgumentException( "Invalid range offsets provided for ByteRange; startOffset=" + startOffset + ", endOffset=" + endOffset); } return new ByteRange(startOffset, endOffset, ByteRangeType.OFFSET_RANGE); } /** * Construct a range from a start offset to the end of an object. * @param startOffset The (inclusive) start byte offset. * @return A {@link ByteRange} with the specified start offset. * @throws IllegalArgumentException */ public static ByteRange fromStartOffset(long startOffset) { if (startOffset < 0) { throw new IllegalArgumentException("Invalid range offsets provided for ByteRange; startOffset=" + startOffset); } return new ByteRange(startOffset, UNDEFINED_OFFSET, ByteRangeType.FROM_START_OFFSET); } /** * Construct a range that represents the last N bytes of an object. * @param lastNBytes the number of bytes to read from the end of an object. * @return A {@link ByteRange} representing the last N bytes of an objects. * @throws IllegalArgumentException */ public static ByteRange fromLastNBytes(long lastNBytes) { if (lastNBytes < 0) { throw new IllegalArgumentException("Invalid range offsets provided for ByteRange; lastNBytes=" + lastNBytes); } return new ByteRange(lastNBytes, UNDEFINED_OFFSET, ByteRangeType.LAST_N_BYTES); } /** * Construct a range from byte offsets. * @param startOffset The (inclusive) start byte offset, or the number of bytes to read from the end of an object, * in the case of a {@link ByteRangeType#LAST_N_BYTES} range type. * @param endOffset The (inclusive) end byte offset, or {@link ByteRange#UNDEFINED_OFFSET}. * @param type The {@link ByteRangeType} for the range. */ private ByteRange(long startOffset, long endOffset, ByteRangeType type) { this.type = type; this.startOffset = startOffset; this.endOffset = endOffset; } /** * Get the start offset for this range. * @return The inclusive start offset for this range. * @throws UnsupportedOperationException if the range does not have a defined start offset (i.e. not of the type * {@link ByteRangeType#OFFSET_RANGE} or * {@link ByteRangeType#FROM_START_OFFSET}) */ public long getStartOffset() { switch (getType()) { case FROM_START_OFFSET: case OFFSET_RANGE: return startOffset; default: throw new UnsupportedOperationException("Cannot get start offset for range type: " + type); } } /** * Get the end offset for this range. * @return The inclusive end offset for this range. * @throws UnsupportedOperationException if the range does not have a defined start offset * (i.e. not of the type {@link ByteRangeType#OFFSET_RANGE}) */ public long getEndOffset() { switch (getType()) { case OFFSET_RANGE: return endOffset; default: throw new UnsupportedOperationException("Cannot get end offset for range type: " + type); } } /** * Get the number of bytes to read from the end of an object. * @return The number of bytes to read from the end of the object. * @throws UnsupportedOperationException if the range is not of the type {@link ByteRangeType#LAST_N_BYTES}) */ public long getLastNBytes() { switch (getType()) { case LAST_N_BYTES: return startOffset; default: throw new UnsupportedOperationException("Cannot get last N bytes for range type: " + type); } } /** * Get the {@link ByteRangeType} for the range. * @return the {@link ByteRangeType} for the range. */ public ByteRangeType getType() { return type; } /** * @return the size of the range, in bytes. * @throws UnsupportedOperationException for {@link ByteRangeType#FROM_START_OFFSET} type ranges. */ public long getRangeSize() { switch (getType()) { case OFFSET_RANGE: return getEndOffset() - getStartOffset() + 1; case LAST_N_BYTES: return getLastNBytes(); default: throw new UnsupportedOperationException("Cannot determine range size for range type: " + type); } } /** * Given the total size of a blob, generate a new {@link ByteRange} of type {@link ByteRangeType#OFFSET_RANGE} with * defined start and end offsets that are verified to be within the supplied total blob size. * @param totalSize the total size of the blob that this range corresponds to. * @return the {@link ByteRange} with start and end offsets * @throws IllegalArgumentException if the byte range exceeds the total size of the blob. */ public ByteRange toResolvedByteRange(long totalSize) { switch (getType()) { case LAST_N_BYTES: if (getLastNBytes() <= totalSize) { return new ByteRange(totalSize - getLastNBytes(), totalSize - 1, ByteRangeType.OFFSET_RANGE); } break; case FROM_START_OFFSET: if (getStartOffset() < totalSize) { return new ByteRange(getStartOffset(), totalSize - 1, ByteRangeType.OFFSET_RANGE); } break; case OFFSET_RANGE: if (getEndOffset() < totalSize) { return new ByteRange(getStartOffset(), getEndOffset(), ByteRangeType.OFFSET_RANGE); } break; } throw new IllegalArgumentException("ByteRange " + this + " exceeds the total blob size " + totalSize); } @Override public String toString() { StringBuilder sb = new StringBuilder("ByteRange{").append("type=").append(type); switch (type) { case LAST_N_BYTES: sb.append(", lastNBytes=").append(getLastNBytes()); break; case FROM_START_OFFSET: sb.append(", startOffset=").append(getStartOffset()); break; case OFFSET_RANGE: sb.append(", startOffset=").append(getStartOffset()).append(", endOffset=").append(getEndOffset()); break; } return sb.append('}').toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ByteRange byteRange = (ByteRange) o; if (startOffset != byteRange.startOffset) { return false; } if (endOffset != byteRange.endOffset) { return false; } return type == byteRange.type; } @Override public int hashCode() { int result = type != null ? type.hashCode() : 0; result = 31 * result + (int) (startOffset ^ (startOffset >>> 32)); result = 31 * result + (int) (endOffset ^ (endOffset >>> 32)); return result; } public enum ByteRangeType { /** * If this range specifies the number of bytes to read from the end of an object. */ LAST_N_BYTES, /** * If this range specifies a start offset to read from to the end of an object. */ FROM_START_OFFSET, /** * If this range specifies a start and end offset to read between. */ OFFSET_RANGE } }