/**
* 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 com.google.common.base.Strings;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manage lookup and creation of unique IDs. IDs are mapped to small unique ids,
* in order to make hbase queries more efficient.
*
* IDs are can be looked up in HBase and cached forever, if needed.
*
* IDs can be of type:
*
* - 1 byte : 256 ids
* - 2 byte short: 65536 ids
* - 4 byte int : 4 294 967 296 ids
* - 8 byte long : enough :)
*
* @author Kristoffer Sjogren
*/
public class UniqueId {
/** mapping name to id. */
public static final byte[] UID_NAME_FAMILY = "n".getBytes();
/** mapping id to name. */
public static final byte[] UID_ID_FAMILY = "i".getBytes();
/** qualifier */
private static final byte[] UID_QUALIFIER = "value".getBytes();
/** size of the id (in bytes). */
private final int width;
/** Row key of the special row used to track the max ID already assigned. */
private static final byte[] MAXID_ROW = { 0 };
/** if mapping between id and name should be cached */
private final boolean shouldCache;
/** cache mapping name to id */
private final ConcurrentHashMap<String, byte[]> idCache = new ConcurrentHashMap<String, byte[]>();
/** cache mapping id to name */
private final ConcurrentHashMap<String, String> nameCache = new ConcurrentHashMap<String, String>();
/** hbase table */
private final HTable htable;
/** charset needed to correctly convert Strings to byte arrays and back. */
private static final Charset CHARSET = Charset.forName("ISO-8859-1");
public UniqueId(byte[] table, int width, Configuration conf, boolean cache) {
this.width = width;
this.shouldCache = cache;
if (width != 1 && width != 2 && width != 4 && width != 8) {
throw new IllegalArgumentException(
"Width must fit in either byte, short, int or long [" + width + "].");
}
try {
this.htable = new HTable(conf, table);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public byte[] getMaxWidth() {
if (width == 1) {
return new byte[] { -1 };
} else if (width == 2) {
return Bytes.fromShort((short) -1);
} else if (width == 4) {
return Bytes.fromInt(-1);
} else {
return Bytes.fromLong(-1L);
}
}
public byte[] getMinWidth() {
if (width == 1) {
return new byte[] { 0 };
} else if (width == 2) {
return Bytes.fromShort((short) 0);
} else if (width == 4) {
return Bytes.fromInt(0);
} else {
return Bytes.fromLong(0);
}
}
public byte[] getId(final String name) {
byte[] id = idCache.get(name);
if (id != null) {
return id;
} else {
id = getIdFromHBase(name);
if (shouldCache) {
idCache.put(name, id);
nameCache.put(new String(id, CHARSET), name);
}
}
return id;
}
public String getName(final byte[] id) {
String strid = new String(id, CHARSET);
String name = nameCache.get(strid);
if (!Strings.isNullOrEmpty(name)) {
return name;
} else {
name = getNameFromHBase(id);
if (shouldCache) {
idCache.put(name, id);
nameCache.put(strid, name);
}
if (Strings.isNullOrEmpty(name)) {
throw new IllegalStateException("Could not map id " + Arrays.toString(id)
+ " to a name from table: " + new String(htable.getTableName()));
}
}
return name;
}
private String getNameFromHBase(byte[] id) {
if (id.length != width) {
throw new IllegalArgumentException("Cannot map name to ids other than (" + width
+ " bytes).");
}
try {
final Get getName = new Get(id);
final Result r = htable.get(getName);
byte[] name = r.getValue(UID_NAME_FAMILY, UID_QUALIFIER);
if (name != null) {
return new String(name, CHARSET);
}
throw new IllegalArgumentException("Failed mapping id [" + toValue(id) + "] to a name.");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private byte[] getIdFromHBase(String name) {
byte[] id;
try {
final Get getId = new Get(name.getBytes(CHARSET));
final Result r = htable.get(getId);
id = r.getValue(UID_ID_FAMILY, UID_QUALIFIER);
if (id != null) {
return id;
}
final Get getMax = new Get(MAXID_ROW);
final byte[] currentMaxId = htable.get(getMax).getValue(UID_ID_FAMILY, UID_QUALIFIER);
id = increaseCounter(currentMaxId);
// update counter
final Put updateMax = new Put(MAXID_ROW);
updateMax.add(UID_ID_FAMILY, UID_QUALIFIER, id);
htable.put(updateMax);
// add id
final Put addId = new Put(id);
addId.add(UID_NAME_FAMILY, UID_QUALIFIER, name.getBytes(CHARSET));
htable.put(addId);
// add name
final Put addName = new Put(name.getBytes(CHARSET));
addName.add(UID_ID_FAMILY, UID_QUALIFIER, id);
htable.put(addName);
htable.flushCommits();
return id;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private byte[] increaseCounter(final byte[] currentMaxId) {
if (width == 1) {
byte counter;
if (currentMaxId != null) {
// increase counter
counter = currentMaxId[0];
} else {
counter = 0;
}
counter++;
return new byte[] { counter };
} else if (width == 2) {
short counter;
if (currentMaxId != null) {
// increase counter
counter = Bytes.getShort(currentMaxId);
} else {
counter = 0;
}
counter++;
return Bytes.fromShort(counter);
} else if (width == 4) {
int counter;
if (currentMaxId != null) {
// increase counter
counter = Bytes.getInt(currentMaxId);
} else {
counter = 0;
}
counter++;
return Bytes.fromInt(counter);
} else if (width == 8) {
long counter;
if (currentMaxId != null) {
// increase counter
counter = Bytes.getLong(currentMaxId);
} else {
counter = 0;
}
counter++;
return Bytes.fromLong(counter);
} else {
throw new IllegalArgumentException("Failure getting value, invalid byte length ["
+ currentMaxId.length + "]");
}
}
public long toValue(byte[] id) {
if (id.length == 1) {
return id[0];
} else if (id.length == 2) {
return Bytes.getShort(id);
} else if (id.length == 4) {
return Bytes.getInt(id);
} else if (id.length == 8) {
return Bytes.getLong(id);
} else {
throw new IllegalArgumentException("Failure getting value, invalid byte length ["
+ id.length + "]");
}
}
}