package org.gbif.occurrence.persistence.hbase; import org.gbif.api.exception.ServiceUnavailableException; import org.gbif.hbase.util.ResultReader; import java.io.IOException; import javax.annotation.Nullable; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.Delete; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.util.Bytes; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** * A convenience class that wraps an HBase table and provides typed get and put operations. * * @param <T> the type of the HBase table's key */ public class HBaseStore<T> { private static final String KEY_CANT_BE_NULL_MSG = "key can't be null"; public static final String HBASE_READ_ERROR_MSG = "Could not read from HBase"; private final TableName tableName; private final String cf; private final byte[] cfBytes; private final Connection connection; // TODO consider a put and get builder that adds columns with successive calls public HBaseStore(String tableName, String cf, Connection connection) { this.tableName = TableName.valueOf(checkNotNull(tableName, "tableName can't be null")); this.cf = checkNotNull(cf, "cf can't be null"); cfBytes = Bytes.toBytes(cf); this.connection = checkNotNull(connection, "connection can't be null"); } public Integer getInt(T key, String columnName) { Result row = getRow(key, columnName); return ResultReader.getInteger(row, cf, columnName, null); } public Long getLong(T key, String columnName) { Result row = getRow(key, columnName); return ResultReader.getLong(row, cf, columnName, null); } public Double getDouble(T key, String columnName) { Result row = getRow(key, columnName); return ResultReader.getDouble(row, cf, columnName, null); } public Float getFloat(T key, String columnName) { Result row = getRow(key, columnName); return ResultReader.getFloat(row, cf, columnName, null); } public String getString(T key, String columnName) { Result row = getRow(key, columnName); return ResultReader.getString(row, cf, columnName, null); } public byte[] getBytes(T key, String columnName) { Result row = getRow(key, columnName); return ResultReader.getBytes(row, cf, columnName, null); } public void putInt(T key, String columnName, int value) { put(key, columnName, Bytes.toBytes(value)); } public void putLong(T key, String columnName, long value) { put(key, columnName, Bytes.toBytes(value)); } public void putFloat(T key, String columnName, float value) { put(key, columnName, Bytes.toBytes(value)); } public void putDouble(T key, String columnName, double value) { put(key, columnName, Bytes.toBytes(value)); } public void putString(T key, String columnName, String value) { put(key, columnName, Bytes.toBytes(value)); } public long incrementColumnValue(T key, String columnName, long value) { checkNotNull(key, KEY_CANT_BE_NULL_MSG); checkNotNull(columnName, "columnName can't be null"); checkNotNull(value, "value can't be null"); long result = 0; try (Table table = connection.getTable(tableName)) { byte[] byteKey = convertKey(key); if (byteKey != null) { result = table.incrementColumnValue(byteKey, cfBytes, Bytes.toBytes(columnName), value); } } catch (IOException e) { throw new ServiceUnavailableException(HBASE_READ_ERROR_MSG, e); } return result; } private void put(T key, String columnName, byte[] value) { checkNotNull(key, KEY_CANT_BE_NULL_MSG); checkNotNull(columnName, "columnName can't be null"); checkNotNull(value, "value can't be null"); try (Table table = connection.getTable(tableName)) { byte[] byteKey = convertKey(key); if (byteKey != null) { Put put = new Put(byteKey); put.addColumn(cfBytes, Bytes.toBytes(columnName), value); table.put(put); } } catch (IOException e) { throw new ServiceUnavailableException(HBASE_READ_ERROR_MSG, e); } } /** * Returns an HBase Result object matching the given key and column name. * * @param key the primary key of the requested row * @param columnName the column value to return * @return HBase Result * * @throws ServiceUnavailableException if there are errors when communicating with HBase */ public Result getRow(T key, String columnName) { checkNotNull(key, KEY_CANT_BE_NULL_MSG); checkNotNull(columnName, "columnName can't be null"); Result row = null; try (Table table = connection.getTable(tableName)) { byte[] byteKey = convertKey(key); if (byteKey != null) { Get get = new Get(byteKey); get.addColumn(cfBytes, Bytes.toBytes(columnName)); row = table.get(get); } } catch (IOException e) { throw new ServiceUnavailableException(HBASE_READ_ERROR_MSG, e); } return row; } /** * Returns an HBase Result object matching the given key. * * @param key the primary key of the requested row * @return HBase Result * * @throws ServiceUnavailableException if there are errors when communicating with HBase */ @Nullable public Result getRow(T key) { checkNotNull(key, KEY_CANT_BE_NULL_MSG); Result row = null; try (Table table = connection.getTable(tableName)) { byte[] byteKey = convertKey(key); if (byteKey != null) { Get get = new Get(byteKey); row = table.get(get); } } catch (IOException e) { throw new ServiceUnavailableException(HBASE_READ_ERROR_MSG, e); } return row; } /** * Do an HBase checkAndPut - a put that will only be attempted if the checkColumn contains the expected checkValue. * * @param key the primary key of the row * @param putColumn the column where the new value will be stored * @param putValue the new value to put * @param checkColumn the column to check * @param checkValue the expected value of the checkColumn * @param ts the timestamp to write on the put (if null, the current timestamp will be used) * @return true if condition was met and put was successful, false otherwise * * @throws ServiceUnavailableException if there are errors when communicating with HBase */ public boolean checkAndPut(T key, String putColumn, byte[] putValue, String checkColumn, @Nullable byte[] checkValue, @Nullable Long ts) { checkNotNull(key, KEY_CANT_BE_NULL_MSG); checkNotNull(putColumn, "putColumn can't be null"); checkNotNull(putValue, "putValue can't be null"); checkNotNull(checkColumn, "checkColumn can't be null"); boolean success = false; try (Table table = connection.getTable(tableName)) { byte[] byteKey = convertKey(key); if (byteKey != null) { Put put = new Put(byteKey); if (ts != null && ts > 0) { put.addColumn(cfBytes, Bytes.toBytes(putColumn), ts, putValue); } else { put.addColumn(cfBytes, Bytes.toBytes(putColumn), putValue); } success = table.checkAndPut(byteKey, cfBytes, Bytes.toBytes(checkColumn), checkValue, put); } } catch (IOException e) { throw new ServiceUnavailableException(HBASE_READ_ERROR_MSG, e); } return success; } // TODO: fix deletions generally and add javadoc public void delete(T key, String... columns) { checkNotNull(key, KEY_CANT_BE_NULL_MSG); checkArgument(columns.length > 0, "columns can't be empty"); try (Table table = connection.getTable(tableName)) { byte[] byteKey = convertKey(key); if (byteKey != null) { Delete delete = new Delete(byteKey); for (String column : columns) { delete.addColumn(cfBytes, Bytes.toBytes(column)); } table.delete(delete); } } catch (IOException e) { throw new ServiceUnavailableException(HBASE_READ_ERROR_MSG, e); } } private byte[] convertKey(T key) { // instanceof is dirty, but it's that or separate classes for different key types if (key instanceof Integer) { return Bytes.toBytes((Integer) key); } else if (key instanceof String) { return Bytes.toBytes((String) key); } else if (key instanceof Long) { return Bytes.toBytes((Long) key); } else if (key instanceof Float) { return Bytes.toBytes((Float) key); } else if (key instanceof Double) { return Bytes.toBytes((Double) key); } return null; } }