package io.eguan.nrs;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* 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.
* #L%
*/
import io.eguan.utils.SimpleIdentifierProvider;
import io.eguan.utils.UuidT;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import com.google.common.base.MoreObjects;
/**
* Immutable header of a {@link NrsFile} or a {@link NrsFileBlock}.
*
* @author oodrive
* @author llambert
* @author pwehrle
*
*/
@Immutable
public final class NrsFileHeader<U> {
/**
* File magic identifying the NRS file format.
*
* This must be found at the start of each file encoded in ASCII with the appropriate byte order.
*/
private static final byte[] MAGIC_VERSION = new byte[] { 'N', 'R', 'S', '1' };
static final int BYTES_PER_LONG = Long.SIZE / Byte.SIZE;
/** Number of blocks per cluster in a {@link NrsFileBlock}. */
private static final int BLOCKS_PER_CLUSER = 4;
/**
* The header length in bytes, including the magic or optional fields.
*/
static final int HEADER_LENGTH = 104;
private NrsFileHeader(final Builder<U> builder) {
super();
this.parentId = Objects.requireNonNull(builder.parentId);
this.deviceId = Objects.requireNonNull(builder.deviceId);
this.nodeId = Objects.requireNonNull(builder.nodeId);
this.fileId = Objects.requireNonNull(builder.fileId);
this.itemSize = builder.itemSize;
this.itemBlockSize = builder.itemBlockSize;
this.hashSize = builder.hashSize;
// Performs sanity checks
if (this.itemBlockSize <= 0) {
throw new IllegalStateException("Invalid block size=" + itemBlockSize);
}
if (this.itemSize < 0) {
throw new IllegalStateException("Invalid size=" + itemSize);
}
if ((this.itemSize % this.itemBlockSize) != 0) {
throw new IllegalStateException("Invalid block size=" + itemBlockSize + ", size=" + itemSize);
}
if (this.hashSize <= 0) {
throw new IllegalStateException("Invalid hash length=" + hashSize);
}
this.clusterSize = builder.clusterSize;
if (this.clusterSize <= 0) {
throw new IllegalStateException("Invalid file cluster size");
}
if (this.clusterSize < this.getHashSize()) {
throw new IllegalStateException("File cluster size less than hash size");
}
int hOneAddressTmp = clusterSize;
// Make sure the H1 header is in the first cluster after the header
while (HEADER_LENGTH > hOneAddressTmp)
hOneAddressTmp += clusterSize;
this.hOneAddress = hOneAddressTmp;
// Compare with the hOneAddress in the header
if (builder.hOneAddress != 0) {
if (builder.hOneAddress != this.hOneAddress) {
throw new IllegalStateException("Invalid H1 address");
}
}
this.timestamp = builder.timestamp;
final Set<NrsFileFlag> flags = builder.flags;
if (flags == null) {
root = false;
partial = false;
blocks = false;
}
else {
root = flags.contains(NrsFileFlag.ROOT);
partial = flags.contains(NrsFileFlag.PARTIAL);
blocks = flags.contains(NrsFileFlag.BLOCKS);
}
}
/**
* The parent item's ID.
*/
private final UuidT<U> parentId;
/**
* The associated device's ID.
*/
private final UUID deviceId;
/**
* The ID of the originating node.
*/
private final UUID nodeId;
/**
* The ID of the file.
*/
private final UuidT<U> fileId;
/**
* The item size in bytes.
*/
private final long itemSize;
/**
* The item block size.
*/
private final int itemBlockSize;
/**
* The item hash key size.
*/
private final int hashSize;
/**
* The cluster size in bytes.
*/
private final int clusterSize;
/**
* The offset at which starts the H1 header.
*/
private final int hOneAddress;
/**
* An time-stamp value set on creation of the file.
*/
private final long timestamp;
/**
* <code>true</code> if the {@link NrsFile} is the root snapshot.
*/
private final boolean root;
/**
* <code>true</code> if the {@link NrsFile} is partial.
*/
private final boolean partial;
/**
* <code>true</code> if the {@link NrsFile} is associated to {@link NrsFileBlock}.
*/
private final boolean blocks;
/**
* Gets the ID of the parent item.
*
* @return the parent item's ID (not <code>null</code>)
*/
public final UuidT<U> getParentId() {
return parentId;
}
/**
* Gets the ID of the device represented by this item.
*
* @return the device ID (not <code>null</code>)
*/
public final UUID getDeviceId() {
return deviceId;
}
/**
* Gets the ID of the originating node of this item.
*
* @return the node ID (not <code>null</code>)
*/
public final UUID getNodeId() {
return nodeId;
}
/**
* Gets the unique ID of the {@link NrsFile}.
*
* @return the uuid of the file (not <code>null</code>)
*/
public final UuidT<U> getFileId() {
return fileId;
}
/**
* Gets the size of the storage volume represented by this item.
*
* @return the size in bytes
*/
public final long getSize() {
return itemSize;
}
/**
* Gets the block size used for addressing blocks of the storage volume.
*
* @return the positive block size in bytes
*/
public final int getBlockSize() {
return itemBlockSize;
}
/**
* The size of the hash keys stored by the persistent item.
*
* @return the positive hash size in bytes
*/
public final int getHashSize() {
return hashSize;
}
/**
* Gets the cluster size.
*
*
* @return the cluster size in bytes
*/
public final int getClusterSize() {
return clusterSize;
}
/**
* Gets the address of the H1 header.
*
*
* @return the H1 address in bytes if set
*/
final int getH1Address() {
return hOneAddress;
}
/**
* Gets the timestamp.
*
*
* @return the timestamp in milliseconds since the epoch
*/
public final long getTimestamp() {
return timestamp;
}
/**
* Tells if the {@link NrsFile} is associated to the root snapshot.
*
* @return <code>true</code> if root
*/
public final boolean isRoot() {
return root;
}
/**
* Tells if the {@link NrsFile} is partial.
*
* @return <code>true</code> if partial
*/
public final boolean isPartial() {
return partial;
}
/**
* Tells if the {@link NrsFile} is associated to {@link NrsFileBlock} file(s).
*
* @return <code>true</code> if has blocks file(s).
*/
public final boolean isBlocks() {
return blocks;
}
/**
* Gets the flags set for the associated {@link NrsFile}.
*
* @return the flags set on the {@link NrsFile}.
*/
public final Set<NrsFileFlag> getFlags() {
final Set<NrsFileFlag> flags = EnumSet.noneOf(NrsFileFlag.class);
if (root)
flags.add(NrsFileFlag.ROOT);
if (partial)
flags.add(NrsFileFlag.PARTIAL);
if (blocks)
flags.add(NrsFileFlag.BLOCKS);
return flags;
}
@Override
public final String toString() {
return MoreObjects.toStringHelper(this).add("parentID", this.getParentId())
.add("deviceID", this.getDeviceId()).add("nodeID", this.getNodeId()).add("fileID", this.getFileId())
.add("size", this.getSize()).add("blockSize", this.getBlockSize())
.add("clusterSize", this.getClusterSize()).add("hashSize", this.getHashSize())
.add("H1Address", this.getH1Address()).add("root", this.isRoot()).add("partial", this.isPartial())
.add("blocks", this.isBlocks()).toString();
}
/**
* Reads the header from a {@link ByteBuffer}.
*
*
* @param inputBuffer
* a buffer respecting the configured file endian-ness
* @return an instance of {@link NrsFileHeader} initialized to the information found in the buffer
* @throws NrsException
* if the buffer's too small or is not a valid header
*/
static final <U> NrsFileHeader<U> readFromBuffer(final ByteBuffer inputBuffer) throws NrsException {
if (inputBuffer.remaining() < HEADER_LENGTH) {
throw new NrsException("Not enough data in header, remaining=" + inputBuffer.remaining() + ", headerlen="
+ HEADER_LENGTH);
}
if (!validateMagic(inputBuffer)) {
throw new NrsException("Invalid magic");
}
final UuidT<U> readParent = readUuidTFromBuffer(inputBuffer);
final UUID readDeviceId = readUuidFromBuffer(inputBuffer);
final UUID readNodeId = readUuidFromBuffer(inputBuffer);
final UuidT<U> readFileId = readUuidTFromBuffer(inputBuffer);
final long readSize = inputBuffer.getLong();
final int readBlockSize = inputBuffer.getInt();
final int readClusterSize = inputBuffer.getInt();
final int readHashSize = inputBuffer.getInt();
final int readH1Address = inputBuffer.getInt();
final long readTimeStamp = inputBuffer.getLong();
final Set<NrsFileFlag> flags = NrsFileFlag.decodeFlags(inputBuffer);
final NrsFileHeader<U> result = new NrsFileHeader.Builder<U>().parent(readParent).device(readDeviceId)
.node(readNodeId).file(readFileId).size(readSize).blockSize(readBlockSize).hashSize(readHashSize)
.clusterSize(readClusterSize).hOneAddress(readH1Address).timestamp(readTimeStamp).flags(flags).build();
return result;
}
/**
* Writes the header to a byte buffer. The buffer length must be at least {@link #HEADER_LENGTH} bytes long.
*
* @param outputBuffer
* the buffer into which the content is written
*/
final void writeToBuffer(final ByteBuffer outputBuffer) {
if (outputBuffer.remaining() < HEADER_LENGTH) {
throw new IllegalArgumentException("Buffer too small=" + outputBuffer.remaining());
}
outputBuffer.put(MAGIC_VERSION);
writeUuidToBuffer(getParentId(), outputBuffer);
writeUuidToBuffer(getDeviceId(), outputBuffer);
writeUuidToBuffer(getNodeId(), outputBuffer);
writeUuidToBuffer(getFileId(), outputBuffer);
outputBuffer.putLong(getSize());
outputBuffer.putInt(getBlockSize());
outputBuffer.putInt(getClusterSize());
outputBuffer.putInt(getHashSize());
outputBuffer.putInt(getH1Address());
outputBuffer.putLong(getTimestamp());
NrsFileFlag.encodeFlags(outputBuffer, getFlags());
}
/**
* Create a NrsFileHeader<NrsFileBlock> for this NrsFileHeader<NrsFile>.
*
* @return a new NrsFileHeader<NrsFileBlock>.
* @throws NrsException
*/
final NrsFileHeader<NrsFileBlock> newBlocksHeader() {
if (!blocks) {
throw new IllegalStateException();
}
final UuidT<NrsFileBlock> blocksParent = new UuidT<>(parentId.getMostSignificantBits(),
parentId.getLeastSignificantBits());
final UuidT<NrsFileBlock> blocksFileId = new UuidT<>(fileId.getMostSignificantBits(),
fileId.getLeastSignificantBits());
final int blocksClusterSize = itemBlockSize * BLOCKS_PER_CLUSER;
// Done not set the flag BLOCKS
final Set<NrsFileFlag> flags = EnumSet.noneOf(NrsFileFlag.class);
if (root)
flags.add(NrsFileFlag.ROOT);
if (partial)
flags.add(NrsFileFlag.PARTIAL);
final NrsFileHeader<NrsFileBlock> result = new NrsFileHeader.Builder<NrsFileBlock>().parent(blocksParent)
.device(deviceId).node(nodeId).file(blocksFileId).size(itemSize).blockSize(itemBlockSize)
.hashSize(hashSize).clusterSize(blocksClusterSize).hOneAddress(0).timestamp(timestamp).flags(flags)
.build();
return result;
}
private static final boolean validateMagic(final ByteBuffer inputBuffer) {
// reads the file magic
final byte[] fileMagic = new byte[MAGIC_VERSION.length];
inputBuffer.get(fileMagic);
return Arrays.equals(fileMagic, MAGIC_VERSION);
}
/**
* Reads an {@link UUID} from the given {@link ByteBuffer}.
*
*
* @param inputBuffer
* the buffer from which to read
* @return the read UUID (never <code>null</code>)
*/
private static final UUID readUuidFromBuffer(final ByteBuffer inputBuffer) {
final long msb = inputBuffer.getLong();
final long lsb = inputBuffer.getLong();
return new UUID(msb, lsb);
}
private static final <U> UuidT<U> readUuidTFromBuffer(final ByteBuffer inputBuffer) {
final long msb = inputBuffer.getLong();
final long lsb = inputBuffer.getLong();
return new UuidT<>(msb, lsb);
}
/**
* Writes the given {@link UUID} to a target {@link ByteBuffer}.
*
*
* The data is always written in standard network byte order (i.e. {@link ByteOrder#BIG_ENDIAN} as specified by <a
* href='http://www.ietf.org/rfc/rfc4122.txt'>RFC4122</a>.
*
* This method will write a 128bit value to the buffer regardless of the
*
* @param uuid
* a valid {@link UUID} or {@code null}
* @param outputBuffer
* the buffer to which to write
*/
private static final void writeUuidToBuffer(final UUID uuid, final ByteBuffer outputBuffer) {
// Writes always in same byte order
outputBuffer.putLong(uuid.getMostSignificantBits());
outputBuffer.putLong(uuid.getLeastSignificantBits());
}
/**
* Same as {@link #writeUuidToBuffer(UUID, ByteBuffer)}, but for {@link UuidT}s.
*
* @param uuid
* @param outputBuffer
*/
private static final <U> void writeUuidToBuffer(final UuidT<U> uuid, final ByteBuffer outputBuffer) {
// Writes always in same byte order
outputBuffer.putLong(uuid.getMostSignificantBits());
outputBuffer.putLong(uuid.getLeastSignificantBits());
}
/**
* Builder class to enforce correct construction of headers.
*
*
*
*/
public static final class Builder<U> {
/**
* The ID of parent item.
*/
private UuidT<U> parentId;
/**
* The associated device's ID.
*/
private UUID deviceId;
/**
* The ID of the originating node.
*/
private UUID nodeId;
/**
* The associated ID of the file.
*/
private UuidT<U> fileId = SimpleIdentifierProvider.newId();
/**
* The item size in bytes.
*/
private long itemSize = -1;
/**
* The item block size.
*/
private int itemBlockSize;
/**
* The item hash key size.
*/
private int hashSize;
/**
* The cluster size in bytes.
*/
private int clusterSize;
/**
* Offset of the H1Header.
*/
private int hOneAddress;
/**
* The cluster size in bytes.
*/
private long timestamp;
/**
* File header flags.
*/
private Set<NrsFileFlag> flags;
Builder() {
super();
}
public final Builder<U> parent(@Nonnull final UuidT<U> parent) {
this.parentId = Objects.requireNonNull(parent);
return this;
}
public final Builder<U> device(@Nonnull final UUID device) {
this.deviceId = Objects.requireNonNull(device);
return this;
}
public final Builder<U> node(@Nonnull final UUID node) {
this.nodeId = Objects.requireNonNull(node);
return this;
}
public final Builder<U> file(@Nonnull final UuidT<U> file) {
this.fileId = Objects.requireNonNull(file);
return this;
}
public final Builder<U> size(final @Nonnegative long size) {
if (size < 0) {
throw new IllegalStateException("Invalid size=" + size);
}
this.itemSize = size;
return this;
}
public final Builder<U> blockSize(final int blockSize) {
this.itemBlockSize = blockSize;
return this;
}
public final Builder<U> hashSize(final int hashSize) {
this.hashSize = hashSize;
return this;
}
/**
* Sets the cluster size.
*
* @param clusterSize
* the cluster size to set in bytes
* @return the modified builder
*/
final Builder<U> clusterSize(final int clusterSize) {
this.clusterSize = clusterSize;
return this;
}
/**
* The cluster size, for test purpose only.
*
* @return the cluster size
*/
final int clusterSize() {
return this.clusterSize;
}
/**
* @param hOneAddress
* @return the modified builder
*/
final Builder<U> hOneAddress(final int hOneAddress) {
this.hOneAddress = hOneAddress;
return this;
}
/**
*
* @param timestamp
* @return the modified builder
*/
public final Builder<U> timestamp(final long timestamp) {
this.timestamp = timestamp;
return this;
}
/**
*
* @param flags
* @return the modified builder
*/
public final Builder<U> flags(final Set<NrsFileFlag> flags) {
this.flags = flags;
return this;
}
public final Builder<U> setFlags(final @Nonnull Set<NrsFileFlag> flags) {
this.flags = EnumSet.noneOf(NrsFileFlag.class);
this.flags.addAll(Objects.requireNonNull(flags));
return this;
}
public final Builder<U> addFlags(final NrsFileFlag... flags) {
if (this.flags == null) {
this.flags = EnumSet.noneOf(NrsFileFlag.class);
}
for (int i = 0; i < flags.length; i++) {
this.flags.add(flags[i]);
}
return this;
}
/**
* builds a new instance.
*
*
* @return the functional new instance
*/
public final NrsFileHeader<U> build() {
return new NrsFileHeader<U>(this);
}
}
}