/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* 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 com.cinchapi.concourse.server.storage.temp;
import java.nio.ByteBuffer;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import com.cinchapi.concourse.server.io.Byteable;
import com.cinchapi.concourse.server.model.PrimaryKey;
import com.cinchapi.concourse.server.model.Text;
import com.cinchapi.concourse.server.model.Value;
import com.cinchapi.concourse.server.storage.Action;
import com.cinchapi.concourse.server.storage.Versioned;
import com.cinchapi.concourse.thrift.TObject;
import com.cinchapi.concourse.time.Time;
import com.cinchapi.concourse.util.ByteBuffers;
/**
* A Write is a {@link Byteable} and {@link Versioned} container that serves as
* a temporary representation of a revision before it is permanently stored and
* indexed.
*
* @author Jeff Nelson
*/
@Immutable
public final class Write implements Byteable, Versioned {
/**
* Return a storable Write that represents a revision to ADD {@code key} as
* {@code value} to {@code record}.
*
* @param key
* @param value
* @param record
* @return the Write
*/
public static Write add(String key, TObject value, long record) {
return new Write(Action.ADD, Text.wrapCached(key), Value.wrap(value),
PrimaryKey.wrap(record), Time.now());
}
/**
* Return the Write encoded in {@code bytes} so long as those bytes adhere
* to the format specified by the {@link #getBytes()} method. This method
* assumes that all the bytes in the {@code bytes} belong to the Value. In
* general, it is necessary to get the appropriate Write slice from the
* parent ByteBuffer using {@link ByteBuffers#slice(ByteBuffer, int, int)}.
*
* @param bytes
* @return the Value
*/
public static Write fromByteBuffer(ByteBuffer bytes) {
int keySize = bytes.getInt();
Action type = Action.values()[bytes.get()];
long version = bytes.getLong();
PrimaryKey record = PrimaryKey.fromByteBuffer(ByteBuffers.get(bytes,
PrimaryKey.SIZE));
Text key = Text.fromByteBuffer(ByteBuffers.get(bytes, keySize));
Value value = Value.fromByteBuffer(ByteBuffers.get(bytes,
bytes.remaining()));
return new Write(type, key, value, record, version);
}
/**
* Return a notStorable Write that represents any revision involving
* {@code key} as {@code value} in {@code record}.
*
* @param key
* @param value
* @param record
* @return the Write
*/
public static Write notStorable(String key, TObject value, long record) {
return new Write(Action.COMPARE, Text.wrapCached(key),
Value.wrap(value), PrimaryKey.wrap(record), NO_VERSION);
}
/**
* Return a storable Write that represents a revision to REMOVE {@code key}
* as {@code value} from {@code record}.
*
* @param key
* @param value
* @param record
* @return the Write
*/
public static Write remove(String key, TObject value, long record) {
return new Write(Action.REMOVE, Text.wrapCached(key),
Value.wrap(value), PrimaryKey.wrap(record), Time.now());
}
/**
* The minimum number of bytes needed to encode every Write.
*/
private static final int CONSTANT_SIZE = PrimaryKey.SIZE + 13; // type(1),
// version(8),
// keySize(4)
/**
* A cached copy of the binary representation that is returned from
* {@link #getBytes()}.
*/
@Nullable
private transient ByteBuffer bytes = null;
private final Text key;
private final PrimaryKey record;
/**
* Indicates the action that generated the Write. The type information is
* recorded so that the Database knows how to apply the Write when accepting
* it from a transport.
*/
private final Action type;
private final Value value;
private final long version;
/**
* Construct a new instance.
*
* @param type
* @param key
* @param value
* @param record
* @param version
*/
private Write(Action type, Text key, Value value, PrimaryKey record,
long version) {
this(type, key, value, record, version, null);
}
/**
* Construct a new instance.
*
* @param type
* @param key
* @param value
* @param record
* @param version
* @param bytes
*/
private Write(Action type, Text key, Value value, PrimaryKey record,
long version, @Nullable ByteBuffer bytes) {
this.type = type;
this.key = key;
this.value = value;
this.record = record;
this.version = version;
this.bytes = bytes;
}
/**
* {@inheritDoc}.
* <p>
* <strong>NOTE:</strong> The Write type is not taken into account when
* determining hashCode or equality. To check for exact matches, including,
* type, use {@link #matches(Write)}.
* </p>
*/
@Override
public boolean equals(Object obj) {
if(obj instanceof Write) {
Write other = (Write) obj;
return value.equals(other.value) && record.equals(other.record)
&& key.equals(other.key);
}
return false;
}
/**
* Return a byte buffer that represents this Write with the following order:
* <ol>
* <li><strong>keySize</strong> - position 0</li>
* <li><strong>type</strong> - position 4</li>
* <li><strong>version</strong> - position 5</li>
* <li><strong>record</strong> - position 13</li>
* <li><strong>key</strong> - position 21</li>
* <li><strong>value</strong> - position(key) + keySize</li>
* </ol>
*
* @return the ByteBuffer representation
*/
@Override
public ByteBuffer getBytes() {
if(bytes == null) {
bytes = ByteBuffer.allocate(size());
copyTo(bytes);
bytes.rewind();
}
return ByteBuffers.asReadOnlyBuffer(bytes);
}
/**
* Return the associated {@code key}.
*
* @return the key
*/
public Text getKey() {
return key;
}
/**
* Return the associated {@code record}.
*
* @return the record
*/
public PrimaryKey getRecord() {
return record;
}
/**
* Return the associated {@code type}.
*
* @return the type
*/
public Action getType() {
return type;
}
/**
* Return the associated {@code value}.
*
* @return the value
*/
public Value getValue() {
return value;
}
@Override
public long getVersion() {
return version;
}
/**
* {@inheritDoc}.
* <p>
* <strong>NOTE:</strong> The Write type is not taken into account when
* determining hashCode or equality. To check for exact matches, including,
* type, use {@link #matches(Write)}.
* </p>
*/
@Override
public int hashCode() {
return Objects.hash(key, value, record);
}
@Override
public boolean isStorable() {
return version != NO_VERSION;
}
/**
* Return a new {@link Write} (with a more recent and unique
* {@link #version} that has the same {@link #key}, {@link #value} and
* {@link #record} components as this write but has the inverse
* {@link Write#type}.
*
* @return a new {@link Write} that is the inverse of this one
*/
public Write inverse() {
if(type == Action.ADD) {
return new Write(Action.REMOVE, key, value, record, Time.now());
}
else if(type == Action.REMOVE) {
return new Write(Action.ADD, key, value, record, Time.now());
}
else {
throw new UnsupportedOperationException(
"Cannot take the inversion of a comparison write");
}
}
/**
* Return {@code true} if this Write and {@code other} have the same
* {@code type} and are equal.
*
* @param other
* @return {@code true} if this matches {@code other}.
*/
public boolean matches(Write other) {
return type == other.type && equals(other);
}
@Override
public int size() {
return CONSTANT_SIZE + key.size() + value.size();
}
@Override
public String toString() {
return type + " " + key + " AS " + value + " IN " + record + " AT "
+ version;
}
@Override
public void copyTo(ByteBuffer buffer) {
buffer.putInt(key.size());
buffer.put((byte) type.ordinal());
buffer.putLong(version);
record.copyTo(buffer);
key.copyTo(buffer);
value.copyTo(buffer);
}
}