/**
* 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.deephacks.confit.internal.hbase;
import org.apache.hadoop.hbase.util.Bytes;
import org.deephacks.confit.internal.hbase.BytesUtils.DataType;
import org.deephacks.confit.internal.hbase.BytesUtils.Reference;
import org.deephacks.confit.internal.hbase.BytesUtils.ReferenceList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.TreeMap;
/**
* Binary KeyValue representation of a bean in HBase.
*
* Every bean is stored in one row with a single qualifier inside a single column family
* for the following reasons:
*
* - Keep column families low, reducing number of HBase store files.
* - Keep qualifiers low, reducing HBase metadata overhead.
* - Make bean writes atomic and versioned/timestamped as a whole.
*
* Each KeyValue begin with a header followed by actual bean data. The header
* store bean metadata that track property ids, types and references for each bean
* and where actual property/reference values are stored in the KeyValue byte array.
*/
public class HBeanKeyValue {
/** 4 byte property id and 4 byte index pointing to the value */
private static final int SIZE_PER_INDEX = 8;
/** each bean is stored in one column family */
public static final byte[] BEAN_COLUMN_FAMILY = "bean".getBytes();
/**
* Used for writing beans to binary form, stored in a KeyValue.
*/
public static class HBeanWriter {
private TreeMap<Integer, BeanProperty> properties = new TreeMap<>();
/**
* Put a collection type property.
*
* @param id id of the property.
* @param collection values
* @param cls type of values in the collection.
*/
public void putValues(int id, Collection<?> collection, Class<?> cls) {
try {
if (collection == null) {
return;
}
DataType type = DataType.getDataType(cls);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int size = collection.size();
switch (type) {
case BYTE:
bytes.write(DataType.BYTE_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write((Byte) o);
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.BYTE_LIST));
break;
case SHORT:
bytes.write(DataType.SHORT_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write(Bytes.toBytes((Short) o));
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.SHORT_LIST));
break;
case INTEGER:
bytes.write(DataType.INTEGER_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write(Bytes.toBytes((Integer) o));
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.INTEGER_LIST));
break;
case LONG:
bytes.write(DataType.LONG_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write(Bytes.toBytes((Long) o));
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.LONG_LIST));
break;
case FLOAT:
bytes.write(DataType.FLOAT_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write(Bytes.toBytes((Float) o));
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.FLOAT_LIST));
break;
case DOUBLE:
bytes.write(DataType.DOUBLE_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write(Bytes.toBytes((Double) o));
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.DOUBLE_LIST));
break;
case BOOLEAN:
bytes.write(DataType.BOOLEAN_LIST.getId());
bytes.write(Bytes.toBytes(size));
for (Object o : collection) {
bytes.write(Bytes.toBytes((Boolean) o));
}
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.BOOLEAN_LIST));
break;
case STRING:
bytes.write(DataType.STRING_LIST.getId());
String[] strings = collection.toArray(new String[collection.size()]);
byte[] byteString = BytesUtils.toBytes(strings);
bytes.write(byteString);
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.STRING_LIST));
break;
default:
throw new UnsupportedOperationException("Did not recognize " + type);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Put a single valued property.
*
* @param id id of the property.
* @param value value of the property
*/
public void putValue(int id, Object value) {
try {
if (value == null) {
return;
}
DataType type = DataType.getDataType(value.getClass());
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
switch (type) {
case BYTE:
bytes.write(DataType.BYTE.getId());
bytes.write((Byte) value);
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.BYTE));
break;
case SHORT:
bytes.write(DataType.SHORT.getId());
bytes.write(Bytes.toBytes((Short)value));
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.SHORT));
break;
case INTEGER:
bytes.write(DataType.INTEGER.getId());
bytes.write(Bytes.toBytes((Integer)value));
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.INTEGER));
break;
case LONG:
bytes.write(DataType.LONG.getId());
bytes.write(Bytes.toBytes((Long)value));
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.LONG));
break;
case FLOAT:
bytes.write(DataType.FLOAT.getId());
bytes.write(Bytes.toBytes((Float)value));
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.FLOAT));
break;
case DOUBLE:
bytes.write(DataType.DOUBLE.getId());
bytes.write(Bytes.toBytes((Double)value));
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.DOUBLE));
break;
case BOOLEAN:
bytes.write(DataType.BOOLEAN.getId());
bytes.write(Bytes.toBytes((Boolean)value));
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.BOOLEAN));
break;
case STRING:
bytes.write(DataType.STRING.getId());
byte[] b = ((String) value).getBytes();
bytes.write(Bytes.toBytes(b.length));
bytes.write(b);
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.STRING));
break;
case REFERENCE:
Reference ref = (Reference) value;
bytes.write(DataType.REFERENCE.getId());
byte[] byteString = BytesUtils.toBytes(ref);
bytes.write(byteString);
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.REFERENCE_LIST));
break;
case REFERENCE_LIST:
ReferenceList list = (ReferenceList) value;
bytes.write(DataType.REFERENCE_LIST.getId());
byteString = BytesUtils.toBytes(list);
bytes.write(byteString);
properties.put(id, new BeanProperty(bytes.toByteArray(), DataType.REFERENCE_LIST));
break;
default:
throw new UnsupportedOperationException("Type not recognized " + type);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @return writes the HBean into binary form.
*/
public byte[] write() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int indexSize = properties.size() * SIZE_PER_INDEX;
int idx = 4 + indexSize;
bytes.write(Bytes.toBytes(idx));
for (Integer id : properties.keySet()) {
BeanProperty property = properties.get(id);
bytes.write(Bytes.toBytes(id));
bytes.write(Bytes.toBytes(idx));
idx += property.bytes.length;
}
for (Integer id : properties.keySet()) {
BeanProperty property = properties.get(id);
bytes.write(property.bytes);
}
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public boolean isBasicType(Class<?> type) {
if(String.class.isAssignableFrom(type)) {
return true;
} else if (Byte.class.isAssignableFrom(type)) {
return true;
} else if (byte.class.isAssignableFrom(type)) {
return true;
} else if (Short.class.isAssignableFrom(type)) {
return true;
} else if (short.class.isAssignableFrom(type)) {
return true;
} else if (Integer.class.isAssignableFrom(type)) {
return true;
} else if (int.class.isAssignableFrom(type)) {
return true;
} else if (Long.class.isAssignableFrom(type)) {
return true;
} else if (long.class.isAssignableFrom(type)) {
return true;
} else if (Float.class.isAssignableFrom(type)) {
return true;
} else if (float.class.isAssignableFrom(type)) {
return true;
} else if (Double.class.isAssignableFrom(type)) {
return true;
} else if (double.class.isAssignableFrom(type)) {
return true;
} else if (Boolean.class.isAssignableFrom(type)) {
return true;
} else if (boolean.class.isAssignableFrom(type)) {
return true;
}
return false;
}
}
/**
* Used for reading a KeyValue binary representation of a bean.
*/
public static class HBeanReader {
/** raw KeyValue data of the bean */
private byte[] data;
/**
* Construct a HBean that was written by a HBeanWriter.
*
* @param data raw KeyValue data.
*/
public HBeanReader(byte[] data) {
this.data = data;
}
public Object getValue(int id) {
int idx = getIndex(id);
if (idx < 0) {
return null;
}
DataType type = DataType.getDataType(data[idx]);
idx = idx + 1;
switch (type) {
case BYTE: return data[idx];
case SHORT: return BytesUtils.getShort(data, idx);
case INTEGER: return BytesUtils.getInt(data, idx);
case LONG: return BytesUtils.getLong(data, idx);
case FLOAT: return BytesUtils.getFloat(data, idx);
case DOUBLE: return BytesUtils.getDouble(data, idx);
case BOOLEAN: return data[idx] != 0;
case STRING: return BytesUtils.getString(data, idx);
case REFERENCE: return BytesUtils.toReference(data, idx);
case BYTE_LIST: return BytesUtils.toByteList(data, idx);
case SHORT_LIST: return BytesUtils.toShortList(data, idx);
case INTEGER_LIST: return BytesUtils.toIntList(data, idx);
case LONG_LIST: return BytesUtils.toLongList(data, idx);
case FLOAT_LIST: return BytesUtils.toFloatList(data, idx);
case DOUBLE_LIST: return BytesUtils.toDoubleList(data, idx);
case BOOLEAN_LIST: return BytesUtils.toBooleanList(data, idx);
case STRING_LIST: return BytesUtils.toStringList(data, idx);
case REFERENCE_LIST: return BytesUtils.toReferences(data, idx);
default:
throw new UnsupportedOperationException("Could not recognize " + type);
}
}
private int getIndex(int id) {
int indexSize = BytesUtils.getInt(data, 0);
if(indexSize <= 4) {
return -1;
}
for (int i = 4; i < indexSize + 4; i += SIZE_PER_INDEX) {
int propertyId = BytesUtils.getInt(data, i);
if (propertyId < id) {
continue;
} else if (propertyId > id) {
return -1;
}
return BytesUtils.getInt(data, i + 4);
}
return -1;
}
public int[] getIds() {
int indexSize = BytesUtils.getInt(data, 0);
int[] ids = new int[indexSize / SIZE_PER_INDEX];
int idx = 0;
for (int i = 4; i < indexSize; i += SIZE_PER_INDEX) {
int propertyId = BytesUtils.getInt(data, i);
ids[idx++] = propertyId;
}
return ids;
}
}
static class BeanProperty {
public byte[] bytes;
public DataType type;
public BeanProperty(byte[] bytes, DataType type) {
this.bytes = bytes;
this.type = type;
}
}
}