/*
* Copyright 2015 by Thomas Lottermann
*
* 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 notaql.engines.hbase.datamodel;
import notaql.datamodel.*;
import notaql.model.EvaluationException;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.*;
/**
* Provides the tools to convert to and from HBase's internal format
*/
public class ValueConverter {
/**
* Generates an ObjectValue from a row. The rows are translated as follows:
*
* colfamily1 colfamily2
* col1 col2 col3
* rowid val1 val2 val3
*
* to
*
* {
* "_id": "rowid",
* "colfamily1": {
* "col1": "val1",
* ...
* }
* ...
* }
*
* @param result
* @return
*/
public static Value convertToNotaQL(Result result) {
final NavigableMap<byte[], NavigableMap<byte[], byte[]>> inCols = result.getNoVersionMap();
final String rowId = Bytes.toString(result.getRow());
final ObjectValue object = new ObjectValue();
object.put(new Step<>("_id"), new StringValue(rowId));
for (Map.Entry<byte[], NavigableMap<byte[], byte[]>> colFamily : inCols.entrySet()) {
final String colFamilyId = Bytes.toString(colFamily.getKey());
final ObjectValue familyObject = new ObjectValue();
for (Map.Entry<byte[], byte[]> col : colFamily.getValue().entrySet()) {
// TODO: This just supports Strings due to HBase's idea of just allowing "Bytes" and not giving any information about the actual type.
// We could solve this by either only allowing Tuples as input which also tell the type OR providing a constructor in NotaQL which specifies the expected type
final String colId = Bytes.toString(col.getKey());
final AtomValue<?> value = ValueUtils.parse(Bytes.toString(col.getValue()));
familyObject.put(new Step<>(colId), value);
}
object.put(new Step<>(colFamilyId), familyObject);
}
return object;
}
public static Put convertFromNotaQL(ObjectValue object) throws IOException {
return convertFromNotaQL(object, null);
}
/**
* Convert object to row by creating a put from it
* @param object
* @param table
* @return
* @throws IOException
*/
public static Put convertFromNotaQL(ObjectValue object, HTable table) throws IOException {
final Value rowIdValue = object.get(new Step<>("_id"));
if(rowIdValue == null)
throw new EvaluationException("Row id was null.");
if(!(rowIdValue instanceof AtomValue<?>))
throw new EvaluationException("Row id may only be of an atomic value");
final String rowId = ((AtomValue<?>)rowIdValue).getValue().toString();
final Put put = new Put(Bytes.toBytes(rowId));
// create metadata in case it doesn't already exist
if(table != null)
createMetaData(object, table);
for (Map.Entry<Step<String>, Value> columnFam : object.toMap().entrySet()) {
if(columnFam.getKey().getStep().equals("_id"))
continue;
final String colFamilyId = columnFam.getKey().getStep();
if(!(columnFam.getValue() instanceof ObjectValue))
throw new EvaluationException("Column families must be of ObjectValues");
final ObjectValue cols = (ObjectValue)columnFam.getValue();
for (Map.Entry<Step<String>, Value> col : cols.toMap().entrySet()) {
final String colId = col.getKey().getStep();
final String value;
if(col.getValue() instanceof AtomValue<?>) {
value = ((AtomValue<?>)col.getValue()).getValue().toString();
} else {
value = col.getValue().toString();
}
put.add(Bytes.toBytes(colFamilyId), Bytes.toBytes(colId), Bytes.toBytes(value));
}
}
return put;
}
public static void createMetaData(ObjectValue object, HTable table) throws IOException {
final HBaseAdmin admin = new HBaseAdmin(table.getConfiguration());
// create non existing column families
for (Step<String> columnFamily : object.keySet()) {
if(columnFamily.getStep().equals("_id"))
continue;
final String columnFamilyId = columnFamily.getStep();
// check if column family exists
boolean exists = false;
for (HColumnDescriptor familyDescriptor : table.getTableDescriptor().getFamilies()) {
if(Bytes.toString(familyDescriptor.getName()).equals(columnFamilyId)) {
exists = true;
break;
}
}
// if not: add it
if(!exists) {
admin.disableTable(table.getTableName());
admin.addColumn(table.getTableName(), new HColumnDescriptor(columnFamilyId));
admin.enableTable(table.getTableName());
}
}
}
public static boolean isEmpty(ObjectValue object) {
return object.toMap().entrySet()
.stream()
.filter(e->e.getValue() instanceof ObjectValue)
.allMatch(e->((ObjectValue)e.getValue()).size() == 0);
}
}