/**
* Copyright 2014 Netflix, 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.netflix.aegisthus.io.writable;
import com.google.common.collect.ComparisonChain;
import org.apache.commons.io.Charsets;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
import javax.annotation.Nonnull;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.Objects;
/**
* This is the key that is output from the map job and input into the reduce job. In most cases it represents a
* key column pair so that the columns are sorted in the same order as they would appear in Cassandra when going into
* the reduce job. This key can also represent a row with no columns. We preserve this information rather than
* deleting these.
*/
public class AegisthusKey implements WritableComparable<AegisthusKey> {
private String sourcePath;
private ByteBuffer key;
private ByteBuffer name;
private Long timestamp;
/**
* This is used to construct an AegisthusKey entry for a row that does not have a column, for example a row with
* all columns deleted.
*
* @param key the row key
*/
public static AegisthusKey createKeyForRow(@Nonnull ByteBuffer key, @Nonnull String sourcePath) {
AegisthusKey aegisthusKey = new AegisthusKey();
aegisthusKey.key = key;
aegisthusKey.sourcePath = sourcePath;
return aegisthusKey;
}
/**
* This is used to construct an AegisthusKey entry for a row that and column pair
*
* @param key the row key
*/
public static AegisthusKey createKeyForRowColumnPair(@Nonnull ByteBuffer key,
@Nonnull String sourcePath,
@Nonnull ByteBuffer name,
long timestamp) {
AegisthusKey aegisthusKey = new AegisthusKey();
aegisthusKey.key = key;
aegisthusKey.sourcePath = sourcePath;
aegisthusKey.name = name;
aegisthusKey.timestamp = timestamp;
return aegisthusKey;
}
@Override
public int compareTo(@Nonnull AegisthusKey other) {
return ComparisonChain.start()
.compare(this.key, other.key)
.compare(this.sourcePath, other.sourcePath)
.result();
}
public int compareTo(@Nonnull AegisthusKey other, Comparator<ByteBuffer> nameComparator) {
// This is a workaround for comparators not handling nulls properly
// The case where name or timestamp is null should only happen when there has been a delete
int result = this.key.compareTo(other.key);
if (result != 0) {
return result;
}
result = this.sourcePath.compareTo(other.sourcePath);
if (result != 0) {
return result;
}
if (this.name == null || this.timestamp == null) {
return -1;
} else if (other.name == null || other.timestamp == null) {
return 1;
}
return ComparisonChain.start()
.compare(this.name, other.name, nameComparator)
.compare(this.timestamp, other.timestamp)
.result();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {return true;}
if (obj == null || getClass() != obj.getClass()) {return false;}
final AegisthusKey other = (AegisthusKey) obj;
return Objects.equals(this.key, other.key)
&& Objects.equals(this.sourcePath, other.sourcePath)
&& Objects.equals(this.name, other.name)
&& Objects.equals(this.timestamp, other.timestamp);
}
@Nonnull
public String getSourcePath() {
return sourcePath;
}
@Nonnull
public ByteBuffer getKey() {
return key;
}
@Override
public int hashCode() {
return Objects.hash(key, sourcePath, name, timestamp);
}
@Override
public void readFields(DataInput dis) throws IOException {
int length = dis.readInt();
byte[] bytes = new byte[length];
dis.readFully(bytes);
this.key = ByteBuffer.wrap(bytes);
// The (possibly empty) sourcePath
length = dis.readInt();
if (length > 0) {
bytes = new byte[length];
dis.readFully(bytes);
this.sourcePath = new String(bytes, Charsets.UTF_8);
} else {
this.sourcePath = "";
}
// Optional column name
length = dis.readInt();
if (length > 0) {
bytes = new byte[length];
dis.readFully(bytes);
this.name = ByteBuffer.wrap(bytes);
} else {
this.name = null;
}
// Optional timestamp
if (dis.readBoolean()) {
this.timestamp = dis.readLong();
} else {
this.timestamp = null;
}
}
/**
* Zero copy readFields.
* Note: As defensive copying is not done, caller should not mutate b1 while using instance.
* */
public void readFields(byte[] bytes, int start, int length) {
int pos = start; // start at the input position
int keyLength = WritableComparator.readInt(bytes, pos);
pos += 4; // move forward by the int that held the key length
this.key = ByteBuffer.wrap(bytes, pos, keyLength);
pos += keyLength; // move forward by the key length
int pathLength = WritableComparator.readInt(bytes, pos);
pos += 4; // move forward by the int that held the path length
if (pathLength > 0) {
this.sourcePath = new String(bytes, pos, pathLength, Charsets.UTF_8);
} else {
this.sourcePath = "";
}
pos += pathLength; // move forward by the path length
int nameLength = WritableComparator.readInt(bytes, pos);
pos += 4; // move forward by an int that held the name length
if (nameLength > 0) {
this.name = ByteBuffer.wrap(bytes, pos, nameLength);
} else {
this.name = null;
}
pos += nameLength; // move forward by the name length
if (bytes[pos] == 0) {
// pos += 1; // move forward by a boolean
this.timestamp = null;
} else {
pos += 1; // move forward by a boolean
this.timestamp = WritableComparator.readLong(bytes, pos);
// pos += 8; // move forward by a long
}
}
@Override
public String toString() {
return com.google.common.base.Objects.toStringHelper(this)
.add("key", key)
.add("name", name)
.add("timestamp", timestamp)
.toString();
}
@Override
public void write(DataOutput dos) throws IOException {
dos.writeInt(key.array().length);
dos.write(key.array());
if (sourcePath.isEmpty()) {
dos.writeInt(0);
} else {
byte[] bytes = sourcePath.getBytes(Charsets.UTF_8);
dos.writeInt(bytes.length);
dos.write(bytes);
}
// Optional column name
if (this.name == null) {
dos.writeInt(0);
} else {
dos.writeInt(name.array().length);
dos.write(name.array());
}
// Optional timestamp
if (this.timestamp != null) {
dos.writeBoolean(true);
dos.writeLong(timestamp);
} else {
dos.writeBoolean(false);
}
}
}