/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.accumulo.core.data;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.accumulo.core.data.thrift.TMutation;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.accumulo.core.util.ByteBufferUtil;
import org.apache.accumulo.core.util.TextUtil;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
/**
* <p>
* Mutation represents an action that manipulates a row in a table. A mutation holds a list of column/value pairs that represent an atomic set of modifications
* to make to a row.
*
* <p>
* Convenience methods which takes columns and value as CharSequence (String implements CharSequence) are provided. CharSequence is converted to UTF-8 by
* constructing a new Text object.
*
* <p>
* When always passing in the same data as a CharSequence/String, its probably more efficient to call the Text put methods. This way the data is only encoded
* once and only one Text object is created.
*
* <p>
* All of the put methods append data to the mutation, they do not overwrite anything that was previously put. The mutation holds a list of all column/values
* that were put into it. The putDelete() methods do not remove something that was previously added to the mutation, rather they indicate that Accumulo should
* insert a delete marker for that row column.
*
*/
public class Mutation implements Writable {
static final int VALUE_SIZE_COPY_CUTOFF = 1 << 15;
private byte[] row;
private byte[] data;
private int entries;
private List<byte[]> values;
// created this little class instead of using ByteArrayOutput stream and DataOutputStream
// because both are synchronized... lots of small syncs slow things down
private static class ByteBuffer {
int offset;
byte data[] = new byte[64];
private void reserve(int l) {
if (offset + l > data.length) {
int newSize = data.length * 2;
while (newSize <= offset + l)
newSize = newSize * 2;
byte[] newData = new byte[newSize];
System.arraycopy(data, 0, newData, 0, offset);
data = newData;
}
}
void add(byte[] b) {
reserve(b.length);
System.arraycopy(b, 0, data, offset, b.length);
offset += b.length;
}
public void add(byte[] bytes, int off, int length) {
reserve(length);
System.arraycopy(bytes, off, data, offset, length);
offset += length;
}
void add(boolean b) {
reserve(1);
if (b)
data[offset++] = 1;
else
data[offset++] = 0;
}
void add(long v) {
reserve(8);
data[offset++] = (byte) (v >>> 56);
data[offset++] = (byte) (v >>> 48);
data[offset++] = (byte) (v >>> 40);
data[offset++] = (byte) (v >>> 32);
data[offset++] = (byte) (v >>> 24);
data[offset++] = (byte) (v >>> 16);
data[offset++] = (byte) (v >>> 8);
data[offset++] = (byte) (v >>> 0);
}
void add(int i) {
reserve(4);
data[offset++] = (byte) (i >>> 24);
data[offset++] = (byte) (i >>> 16);
data[offset++] = (byte) (i >>> 8);
data[offset++] = (byte) (i >>> 0);
}
public byte[] toArray() {
byte ret[] = new byte[offset];
System.arraycopy(data, 0, ret, 0, offset);
return ret;
}
}
private static class SimpleReader {
int offset;
byte data[];
SimpleReader(byte b[]) {
this.data = b;
}
int readInt() {
return (data[offset++] << 24) + ((data[offset++] & 255) << 16) + ((data[offset++] & 255) << 8) + ((data[offset++] & 255) << 0);
}
long readLong() {
return (((long) data[offset++] << 56) + ((long) (data[offset++] & 255) << 48) + ((long) (data[offset++] & 255) << 40)
+ ((long) (data[offset++] & 255) << 32) + ((long) (data[offset++] & 255) << 24) + ((data[offset++] & 255) << 16) + ((data[offset++] & 255) << 8) + ((data[offset++] & 255) << 0));
}
void readBytes(byte b[]) {
System.arraycopy(data, offset, b, 0, b.length);
offset += b.length;
}
boolean readBoolean() {
return (data[offset++] == 1);
}
}
private ByteBuffer buffer;
private List<ColumnUpdate> updates;
private static final byte[] EMPTY_BYTES = new byte[0];
private void serialize() {
if (buffer != null) {
data = buffer.toArray();
buffer = null;
}
}
public Mutation(Text row) {
this.row = new byte[row.getLength()];
System.arraycopy(row.getBytes(), 0, this.row, 0, row.getLength());
buffer = new ByteBuffer();
}
public Mutation(CharSequence row) {
this(new Text(row.toString()));
}
public Mutation() {}
public Mutation(TMutation tmutation) {
this.row = ByteBufferUtil.toBytes(tmutation.row);
this.data = ByteBufferUtil.toBytes(tmutation.data);
this.entries = tmutation.entries;
this.values = ByteBufferUtil.toBytesList(tmutation.values);
if (this.row == null) {
throw new IllegalArgumentException("null row");
}
if (this.data == null) {
throw new IllegalArgumentException("null serialized data");
}
}
public Mutation(Mutation m) {
m.serialize();
this.row = m.row;
this.data = m.data;
this.entries = m.entries;
this.values = m.values;
}
public byte[] getRow() {
return row;
}
private void put(byte b[]) {
buffer.add(b.length);
buffer.add(b);
}
private void put(Text t) {
buffer.add(t.getLength());
buffer.add(t.getBytes(), 0, t.getLength());
}
private void put(boolean b) {
buffer.add(b);
}
private void put(int i) {
buffer.add(i);
}
private void put(long l) {
buffer.add(l);
}
private void put(Text cf, Text cq, byte[] cv, boolean hasts, long ts, boolean deleted, byte[] val) {
if (buffer == null)
throw new IllegalStateException("Can not add to mutation after serializing it");
put(cf);
put(cq);
put(cv);
put(hasts);
put(ts);
put(deleted);
if (val.length < VALUE_SIZE_COPY_CUTOFF) {
put(val);
} else {
if (values == null)
values = new ArrayList<byte[]>();
byte copy[] = new byte[val.length];
System.arraycopy(val, 0, copy, 0, val.length);
values.add(copy);
put(-1 * values.size());
}
entries++;
}
private void put(CharSequence cf, CharSequence cq, byte[] cv, boolean hasts, long ts, boolean deleted, byte[] val) {
put(new Text(cf.toString()), new Text(cq.toString()), cv, hasts, ts, deleted, val);
}
private void put(CharSequence cf, CharSequence cq, byte[] cv, boolean hasts, long ts, boolean deleted, CharSequence val) {
put(cf, cq, cv, hasts, ts, deleted, TextUtil.getBytes(new Text(val.toString())));
}
public void put(Text columnFamily, Text columnQualifier, Value value) {
put(columnFamily, columnQualifier, EMPTY_BYTES, false, 0l, false, value.get());
}
public void put(Text columnFamily, Text columnQualifier, ColumnVisibility columnVisibility, Value value) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), false, 0l, false, value.get());
}
public void put(Text columnFamily, Text columnQualifier, long timestamp, Value value) {
put(columnFamily, columnQualifier, EMPTY_BYTES, true, timestamp, false, value.get());
}
public void put(Text columnFamily, Text columnQualifier, ColumnVisibility columnVisibility, long timestamp, Value value) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), true, timestamp, false, value.get());
}
public void putDelete(Text columnFamily, Text columnQualifier) {
put(columnFamily, columnQualifier, EMPTY_BYTES, false, 0l, true, EMPTY_BYTES);
}
public void putDelete(Text columnFamily, Text columnQualifier, ColumnVisibility columnVisibility) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), false, 0l, true, EMPTY_BYTES);
}
public void putDelete(Text columnFamily, Text columnQualifier, long timestamp) {
put(columnFamily, columnQualifier, EMPTY_BYTES, true, timestamp, true, EMPTY_BYTES);
}
public void putDelete(Text columnFamily, Text columnQualifier, ColumnVisibility columnVisibility, long timestamp) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), true, timestamp, true, EMPTY_BYTES);
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, Value value) {
put(columnFamily, columnQualifier, EMPTY_BYTES, false, 0l, false, value.get());
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, ColumnVisibility columnVisibility, Value value) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), false, 0l, false, value.get());
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, long timestamp, Value value) {
put(columnFamily, columnQualifier, EMPTY_BYTES, true, timestamp, false, value.get());
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, ColumnVisibility columnVisibility, long timestamp, Value value) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), true, timestamp, false, value.get());
}
public void putDelete(CharSequence columnFamily, CharSequence columnQualifier) {
put(columnFamily, columnQualifier, EMPTY_BYTES, false, 0l, true, EMPTY_BYTES);
}
public void putDelete(CharSequence columnFamily, CharSequence columnQualifier, ColumnVisibility columnVisibility) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), false, 0l, true, EMPTY_BYTES);
}
public void putDelete(CharSequence columnFamily, CharSequence columnQualifier, long timestamp) {
put(columnFamily, columnQualifier, EMPTY_BYTES, true, timestamp, true, EMPTY_BYTES);
}
public void putDelete(CharSequence columnFamily, CharSequence columnQualifier, ColumnVisibility columnVisibility, long timestamp) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), true, timestamp, true, EMPTY_BYTES);
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, CharSequence value) {
put(columnFamily, columnQualifier, EMPTY_BYTES, false, 0l, false, value);
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, ColumnVisibility columnVisibility, CharSequence value) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), false, 0l, false, value);
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, long timestamp, CharSequence value) {
put(columnFamily, columnQualifier, EMPTY_BYTES, true, timestamp, false, value);
}
public void put(CharSequence columnFamily, CharSequence columnQualifier, ColumnVisibility columnVisibility, long timestamp, CharSequence value) {
put(columnFamily, columnQualifier, columnVisibility.getExpression(), true, timestamp, false, value);
}
private byte[] readBytes(SimpleReader in) {
int len = in.readInt();
if (len == 0)
return EMPTY_BYTES;
byte bytes[] = new byte[len];
in.readBytes(bytes);
return bytes;
}
public List<ColumnUpdate> getUpdates() {
serialize();
SimpleReader in = new SimpleReader(data);
if (updates == null) {
if (entries == 1) {
updates = Collections.singletonList(deserializeColumnUpdate(in));
} else {
ColumnUpdate[] tmpUpdates = new ColumnUpdate[entries];
for (int i = 0; i < entries; i++)
tmpUpdates[i] = deserializeColumnUpdate(in);
updates = Arrays.asList(tmpUpdates);
}
}
return updates;
}
private ColumnUpdate deserializeColumnUpdate(SimpleReader in) {
byte[] cf = readBytes(in);
byte[] cq = readBytes(in);
byte[] cv = readBytes(in);
boolean hasts = in.readBoolean();
int tso = in.offset;
long ts = in.readLong();
boolean deleted = in.readBoolean();
byte[] val;
int valLen = in.readInt();
if (valLen < 0) {
val = values.get((-1 * valLen) - 1);
} else if (valLen == 0) {
val = EMPTY_BYTES;
} else {
val = new byte[valLen];
in.readBytes(val);
}
return new ColumnUpdate(cf, cq, cv, hasts, ts, deleted, val, data, tso);
}
private int cachedValLens = -1;
long getValueLengths() {
if (values == null)
return 0;
if (cachedValLens == -1) {
int tmpCVL = 0;
for (byte val[] : values)
tmpCVL += val.length;
cachedValLens = tmpCVL;
}
return cachedValLens;
}
public long numBytes() {
serialize();
return row.length + data.length + getValueLengths();
}
public long estimatedMemoryUsed() {
return numBytes() + 230;
}
/**
* @return the number of column value pairs added to the mutation
*/
public int size() {
return entries;
}
@Override
public void readFields(DataInput in) throws IOException {
// Clear out cached column updates and value lengths so
// that we recalculate them based on the (potentially) new
// data we are about to read in.
updates = null;
cachedValLens = -1;
buffer = null;
int len = in.readInt();
row = new byte[len];
in.readFully(row);
len = in.readInt();
data = new byte[len];
in.readFully(data);
entries = in.readInt();
boolean valuesPresent = in.readBoolean();
if (!valuesPresent) {
values = null;
} else {
values = new ArrayList<byte[]>();
int numValues = in.readInt();
for (int i = 0; i < numValues; i++) {
len = in.readInt();
byte val[] = new byte[len];
in.readFully(val);
values.add(val);
}
}
}
@Override
public void write(DataOutput out) throws IOException {
serialize();
out.writeInt(row.length);
out.write(row);
out.writeInt(data.length);
out.write(data);
out.writeInt(entries);
if (values == null)
out.writeBoolean(false);
else {
out.writeBoolean(true);
out.writeInt(values.size());
for (int i = 0; i < values.size(); i++) {
byte val[] = values.get(i);
out.writeInt(val.length);
out.write(val);
}
}
}
@Override
public boolean equals(Object o) {
if (o instanceof Mutation)
return equals((Mutation) o);
return false;
}
@Override
public int hashCode() {
return toThrift().hashCode();
}
public boolean equals(Mutation m) {
serialize();
m.serialize();
if (Arrays.equals(row, m.row) && entries == m.entries && Arrays.equals(data, m.data)) {
if (values == null && m.values == null)
return true;
if (values != null && m.values != null && values.size() == m.values.size()) {
for (int i = 0; i < values.size(); i++) {
if (!Arrays.equals(values.get(i), m.values.get(i)))
return false;
}
return true;
}
}
return false;
}
public TMutation toThrift() {
serialize();
return new TMutation(java.nio.ByteBuffer.wrap(row), java.nio.ByteBuffer.wrap(data), ByteBufferUtil.toByteBuffers(values), entries);
}
}