/*
* Copyright 2016 The Simple File Server Authors
*
* 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.
*/
package org.sfs.vo;
import com.google.common.base.Optional;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Iterables.addAll;
import static com.google.common.hash.Hashing.md5;
import static com.google.common.hash.Hashing.sha512;
import static java.lang.Boolean.TRUE;
public abstract class Segment<T extends Segment> implements Identity {
public static final byte[] EMPTY_MD5 = md5().hashBytes(new byte[]{}).asBytes();
public static final byte[] EMPTY_SHA512 = sha512().hashBytes(new byte[]{}).asBytes();
private final XVersion parent;
private final long id;
private Long writeLength;
private Long readLength;
private byte[] readMd5;
private byte[] readSha512;
private byte[] writeSha512;
private byte[] tinyData;
private Boolean isTinyData;
private Boolean isTinyDataDeleted;
private SegmentCipher segmentCipher;
private List<TransientBlobReference> blobs = new ArrayList<>();
public Segment(XVersion parent, long id) {
this.parent = parent;
checkState(id >= 0, "Id must be >= 0");
this.id = id;
}
public XVersion<? extends XVersion> getParent() {
return parent;
}
@Override
public long getId() {
return id;
}
public Optional<Long> getReadLength() {
return fromNullable(readLength);
}
public T setReadLength(Long readLength) {
this.readLength = readLength;
return (T) this;
}
public Optional<byte[]> getReadMd5() {
return fromNullable(readMd5);
}
public T setReadMd5(byte[] readMd5) {
this.readMd5 = readMd5;
return (T) this;
}
public Optional<byte[]> getReadSha512() {
return fromNullable(readSha512);
}
public T setReadSha512(byte[] readSha512) {
this.readSha512 = readSha512;
return (T) this;
}
public Optional<byte[]> getWriteSha512() {
return fromNullable(writeSha512);
}
public T setWriteSha512(byte[] writeSha512) {
this.writeSha512 = writeSha512;
return (T) this;
}
public Optional<Long> getWriteLength() {
return fromNullable(writeLength);
}
public T setWriteLength(Long writeLength) {
this.writeLength = writeLength;
return (T) this;
}
public Optional<SegmentCipher> getSegmentCipher() {
return fromNullable(segmentCipher);
}
public T setSegmentCipher(SegmentCipher segmentCipher) {
this.segmentCipher = segmentCipher;
return (T) this;
}
public List<TransientBlobReference> getBlobs() {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
return blobs;
}
public T setBlobs(Iterable<TransientBlobReference> blobs) {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
this.blobs.clear();
addAll(this.blobs, blobs);
return (T) this;
}
public T removeBlobs(Collection<TransientBlobReference> blobs) {
this.blobs.removeAll(blobs);
return (T) this;
}
public TransientBlobReference newBlob() {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
TransientBlobReference blobReference = new TransientBlobReference(this);
this.blobs.add(blobReference);
return blobReference;
}
public Iterable<TransientBlobReference> verifiedAckdBlobs() {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
return from(verifiedBlobs())
.filter(BlobReference::isAcknowledged);
}
public Iterable<TransientBlobReference> verifiedUnAckdBlobs() {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
return from(verifiedBlobs())
.filter(input -> !input.isAcknowledged());
}
private Iterable<TransientBlobReference> verifiedBlobs() {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
return from(blobs)
.filter(notNull())
.filter(blob -> {
Optional<String> oVolumeId = blob.getVolumeId();
Optional<Long> oPosition = blob.getPosition();
Optional<byte[]> oComputedSha512 = blob.getReadSha512();
Optional<Long> oComputedLength = blob.getReadLength();
boolean deleted = blob.isDeleted();
boolean hasVolumeId = oVolumeId.isPresent();
boolean hasPosition = oPosition.isPresent();
boolean sha512Match = oComputedSha512.isPresent() && Arrays.equals(writeSha512, oComputedSha512.get());
boolean lengthMatch = oComputedLength.isPresent() && writeLength != null && writeLength.equals(oComputedLength.get());
boolean noNullFields = writeLength != null && readLength != null && readMd5 != null && readSha512 != null && writeSha512 != null;
return !deleted && hasVolumeId && hasPosition && sha512Match && lengthMatch && noNullFields;
});
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Segment)) return false;
Segment segment = (Segment) o;
return id == segment.id;
}
public byte[] getTinyData() {
return tinyData;
}
public T setTinyData(byte[] tinyData) {
checkState(TRUE.equals(isTinyData), "isTinyData must be set to true");
this.tinyData = tinyData;
return (T) this;
}
public boolean isTinyData() {
return TRUE.equals(isTinyData);
}
public T setIsTinyData(Boolean tinyData) {
isTinyData = tinyData;
return (T) this;
}
public boolean isTinyDataDeleted() {
return TRUE.equals(isTinyDataDeleted);
}
public T deleteTinyData() {
checkState(TRUE.equals(isTinyData), "isTinyData must be set to true");
isTinyDataDeleted = true;
return (T) this;
}
@Override
public int hashCode() {
return (int) (id ^ (id >>> 32));
}
public JsonObject toJsonObject() {
JsonObject document = new JsonObject();
document.put("id", id);
document.put("read_md5", readMd5);
document.put("read_sha512", readSha512);
document.put("read_length", readLength);
document.put("write_sha512", writeSha512);
document.put("write_length", writeLength);
document.put("tiny_data", tinyData);
document.put("is_tiny_data", TRUE.equals(isTinyData));
document.put("is_tiny_data_deleted", TRUE.equals(isTinyDataDeleted));
if (tinyData != null) {
checkState(TRUE.equals(isTinyData), "isTinyData must be set to true");
checkState(blobs == null || blobs.isEmpty(), "blobs must be empty when tinyData exists");
}
if (blobs != null && !blobs.isEmpty()) {
checkState(!TRUE.equals(isTinyData), "isTinyData must be set to false");
checkState(tinyData == null, "tinyData must be empty when blobs exist");
}
if (segmentCipher != null) {
document = document
.put("container_key_id", segmentCipher.containerKeyId)
.put("cipher_salt", segmentCipher.salt);
} else {
document.put("container_key_id", (String) null)
.put("cipher_salt", (byte[]) null);
}
JsonArray blobJsonArray = new JsonArray();
for (TransientBlobReference transientBlobReference : blobs) {
blobJsonArray.add(transientBlobReference.toJsonObject());
}
document.put("blobs", blobJsonArray);
return document;
}
public T merge(JsonObject document) {
Long id = document.getLong("id");
checkNotNull(id, "id cannot be null");
checkState(id == this.id, "id was %s, expected %s", id, this.id);
setReadMd5(document.getBinary("read_md5"));
setReadSha512(document.getBinary("read_sha512"));
setReadLength(document.getLong("read_length"));
setWriteSha512(document.getBinary("write_sha512"));
setWriteLength(document.getLong("write_length"));
isTinyData = document.getBoolean("is_tiny_data");
tinyData = document.getBinary("tiny_data");
isTinyDataDeleted = document.getBoolean("is_tiny_data_deleted");
String cipherKey = document.getString("container_key_id");
byte[] cipherSalt = document.getBinary("cipher_salt");
segmentCipher = new SegmentCipher(cipherKey, cipherSalt);
JsonArray blobJsonArray = document.getJsonArray("blobs");
this.blobs.clear();
if (blobJsonArray != null) {
for (Object o : blobJsonArray) {
JsonObject jsonObject = (JsonObject) o;
TransientBlobReference transientBlobReference = new TransientBlobReference(this).merge(jsonObject);
this.blobs.add(transientBlobReference);
}
}
return (T) this;
}
public static class SegmentCipher {
private String containerKeyId;
private byte[] salt;
public SegmentCipher(String containerKeyId, byte[] salt) {
this.containerKeyId = containerKeyId;
this.salt = salt;
}
public Optional<String> getContainerKeyId() {
return fromNullable(containerKeyId);
}
public SegmentCipher setContainerKeyId(String containerKeyId) {
this.containerKeyId = containerKeyId;
return this;
}
public Optional<byte[]> getSalt() {
return fromNullable(salt);
}
public SegmentCipher setSalt(byte[] salt) {
this.salt = salt;
return this;
}
}
}