// This file is part of OpenTSDB.
// Copyright (C) 2010-2012 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.core;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
import org.hbase.async.Bytes;
import org.hbase.async.Bytes.ByteMap;
import net.opentsdb.query.filter.TagVFilter;
import net.opentsdb.uid.NoSuchUniqueId;
import net.opentsdb.uid.NoSuchUniqueName;
import net.opentsdb.utils.Pair;
/** Helper functions to deal with tags. */
public final class Tags {
private static final Logger LOG = LoggerFactory.getLogger(Tags.class);
private static String allowSpecialChars = "";
private Tags() {
// Can't create instances of this utility class.
}
/**
* Optimized version of {@code String#split} that doesn't use regexps.
* This function works in O(5n) where n is the length of the string to
* split.
* @param s The string to split.
* @param c The separator to use to split the string.
* @return A non-null, non-empty array.
*/
public static String[] splitString(final String s, final char c) {
final char[] chars = s.toCharArray();
int num_substrings = 1;
for (final char x : chars) {
if (x == c) {
num_substrings++;
}
}
final String[] result = new String[num_substrings];
final int len = chars.length;
int start = 0; // starting index in chars of the current substring.
int pos = 0; // current index in chars.
int i = 0; // number of the current substring.
for (; pos < len; pos++) {
if (chars[pos] == c) {
result[i++] = new String(chars, start, pos - start);
start = pos + 1;
}
}
result[i] = new String(chars, start, pos - start);
return result;
}
/**
* Parses a tag into a HashMap.
* @param tags The HashMap into which to store the tag.
* @param tag A String of the form "tag=value".
* @throws IllegalArgumentException if the tag is malformed.
* @throws IllegalArgumentException if the tag was already in tags with a
* different value.
*/
public static void parse(final HashMap<String, String> tags,
final String tag) {
final String[] kv = splitString(tag, '=');
if (kv.length != 2 || kv[0].length() <= 0 || kv[1].length() <= 0) {
throw new IllegalArgumentException("invalid tag: " + tag);
}
if (kv[1].equals(tags.get(kv[0]))) {
return;
}
if (tags.get(kv[0]) != null) {
throw new IllegalArgumentException("duplicate tag: " + tag
+ ", tags=" + tags);
}
tags.put(kv[0], kv[1]);
}
/**
* Parses a tag into a list of key/value pairs, allowing nulls for either
* value.
* @param tags The list into which the parsed tag should be stored
* @param tag A string of the form "tag=value" or "=value" or "tag="
* @throws IllegalArgumentException if the tag is malformed.
* @since 2.1
*/
public static void parse(final List<Pair<String, String>> tags,
final String tag) {
if (tag == null || tag.isEmpty() || tag.length() < 2) {
throw new IllegalArgumentException("Missing tag pair");
}
if (tag.charAt(0) == '=') {
tags.add(new Pair<String, String>(null, tag.substring(1)));
return;
} else if (tag.charAt(tag.length() - 1) == '=') {
tags.add(new Pair<String, String>(tag.substring(0, tag.length() - 1), null));
return;
}
final String[] kv = splitString(tag, '=');
if (kv.length != 2 || kv[0].length() <= 0 || kv[1].length() <= 0) {
throw new IllegalArgumentException("invalid tag: " + tag);
}
tags.add(new Pair<String, String>(kv[0], kv[1]));
}
/**
* Parses the metric and tags out of the given string.
* @param metric A string of the form "metric" or "metric{tag=value,...}".
* @param tags The map to populate with the tags parsed out of the first
* argument.
* @return The name of the metric.
* @throws IllegalArgumentException if the metric is malformed.
*/
public static String parseWithMetric(final String metric,
final HashMap<String, String> tags) {
final int curly = metric.indexOf('{');
if (curly < 0) {
return metric;
}
final int len = metric.length();
if (metric.charAt(len - 1) != '}') { // "foo{"
throw new IllegalArgumentException("Missing '}' at the end of: " + metric);
} else if (curly == len - 2) { // "foo{}"
return metric.substring(0, len - 2);
}
// substring the tags out of "foo{a=b,...,x=y}" and parse them.
for (final String tag : splitString(metric.substring(curly + 1, len - 1),
',')) {
try {
parse(tags, tag);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("When parsing tag '" + tag
+ "': " + e.getMessage());
}
}
// Return the "foo" part of "foo{a=b,...,x=y}"
return metric.substring(0, curly);
}
/**
* Parses an optional metric and tags out of the given string, any of
* which may be null. Requires at least one metric, tagk or tagv.
* @param metric A string of the form "metric" or "metric{tag=value,...}"
* or even "{tag=value,...}" where the metric may be missing.
* @param tags The list to populate with parsed tag pairs
* @return The name of the metric if it exists, null otherwise
* @throws IllegalArgumentException if the metric is malformed.
* @since 2.1
*/
public static String parseWithMetric(final String metric,
final List<Pair<String, String>> tags) {
final int curly = metric.indexOf('{');
if (curly < 0) {
if (metric.isEmpty()) {
throw new IllegalArgumentException("Metric string was empty");
}
return metric;
}
final int len = metric.length();
if (metric.charAt(len - 1) != '}') { // "foo{"
throw new IllegalArgumentException("Missing '}' at the end of: " + metric);
} else if (curly == len - 2) { // "foo{}"
if (metric.charAt(0) == '{') {
throw new IllegalArgumentException("Missing metric and tags: " + metric);
}
return metric.substring(0, len - 2);
}
// substring the tags out of "foo{a=b,...,x=y}" and parse them.
for (final String tag : splitString(metric.substring(curly + 1, len - 1),
',')) {
try {
parse(tags, tag);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("When parsing tag '" + tag
+ "': " + e.getMessage());
}
}
// Return the "foo" part of "foo{a=b,...,x=y}"
if (metric.charAt(0) == '{') {
return null;
}
return metric.substring(0, curly);
}
/**
* Parses the metric and tags out of the given string.
* @param metric A string of the form "metric" or "metric{tag=value,...}" or
* now "metric{groupby=filter}{filter=filter}".
* @param filters A list of filters to write the results to. May not be null
* @return The name of the metric.
* @throws IllegalArgumentException if the metric is malformed or the filter
* list is null.
* @since 2.2
*/
public static String parseWithMetricAndFilters(final String metric,
final List<TagVFilter> filters) {
if (metric == null || metric.isEmpty()) {
throw new IllegalArgumentException("Metric cannot be null or empty");
}
if (filters == null) {
throw new IllegalArgumentException("Filters cannot be null");
}
final int curly = metric.indexOf('{');
if (curly < 0) {
return metric;
}
final int len = metric.length();
if (metric.charAt(len - 1) != '}') { // "foo{"
throw new IllegalArgumentException("Missing '}' at the end of: " + metric);
} else if (curly == len - 2) { // "foo{}"
return metric.substring(0, len - 2);
}
final int close = metric.indexOf('}');
final HashMap<String, String> filter_map = new HashMap<String, String>();
if (close != metric.length() - 1) { // "foo{...}{tagk=filter}"
final int filter_bracket = metric.lastIndexOf('{');
for (final String filter : splitString(metric.substring(filter_bracket + 1,
metric.length() - 1), ',')) {
if (filter.isEmpty()) {
break;
}
filter_map.clear();
try {
parse(filter_map, filter);
TagVFilter.mapToFilters(filter_map, filters, false);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("When parsing filter '" + filter
+ "': " + e.getMessage(), e);
}
}
}
// substring the tags out of "foo{a=b,...,x=y}" and parse them.
for (final String tag : splitString(metric.substring(curly + 1, close), ',')) {
try {
if (tag.isEmpty() && close != metric.length() - 1){
break;
}
filter_map.clear();
parse(filter_map, tag);
TagVFilter.tagsToFilters(filter_map, filters);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("When parsing tag '" + tag
+ "': " + e.getMessage(), e);
}
}
// Return the "foo" part of "foo{a=b,...,x=y}"
return metric.substring(0, curly);
}
/**
* Parses an integer value as a long from the given character sequence.
* <p>
* This is equivalent to {@link Long#parseLong(String)} except it's up to
* 100% faster on {@link String} and always works in O(1) space even with
* {@link StringBuilder} buffers (where it's 2x to 5x faster).
* @param s The character sequence containing the integer value to parse.
* @return The value parsed.
* @throws NumberFormatException if the value is malformed or overflows.
*/
public static long parseLong(final CharSequence s) {
final int n = s.length(); // Will NPE if necessary.
if (n == 0) {
throw new NumberFormatException("Empty string");
}
char c = s.charAt(0); // Current character.
int i = 1; // index in `s'.
if (c < '0' && (c == '+' || c == '-')) { // Only 1 test in common case.
if (n == 1) {
throw new NumberFormatException("Just a sign, no value: " + s);
} else if (n > 20) { // "+9223372036854775807" or "-9223372036854775808"
throw new NumberFormatException("Value too long: " + s);
}
c = s.charAt(1);
i = 2; // Skip over the sign.
} else if (n > 19) { // "9223372036854775807"
throw new NumberFormatException("Value too long: " + s);
}
long v = 0; // The result (negated to easily handle MIN_VALUE).
do {
if ('0' <= c && c <= '9') {
v -= c - '0';
} else {
throw new NumberFormatException("Invalid character '" + c
+ "' in " + s);
}
if (i == n) {
break;
}
v *= 10;
c = s.charAt(i++);
} while (true);
if (v > 0) {
throw new NumberFormatException("Overflow in " + s);
} else if (s.charAt(0) == '-') {
return v; // Value is already negative, return unchanged.
} else if (v == Long.MIN_VALUE) {
throw new NumberFormatException("Overflow in " + s);
} else {
return -v; // Positive value, need to fix the sign.
}
}
/**
* Extracts the value of the given tag name from the given row key.
* @param tsdb The TSDB instance to use for UniqueId lookups.
* @param row The row key in which to search the tag name.
* @param name The name of the tag to search in the row key.
* @return The value associated with the given tag name, or null if this tag
* isn't present in this row key.
*/
static String getValue(final TSDB tsdb, final byte[] row,
final String name) throws NoSuchUniqueName {
validateString("tag name", name);
final byte[] id = tsdb.tag_names.getId(name);
final byte[] value_id = getValueId(tsdb, row, id);
if (value_id == null) {
return null;
}
// This shouldn't throw a NoSuchUniqueId.
try {
return tsdb.tag_values.getName(value_id);
} catch (NoSuchUniqueId e) {
LOG.error("Internal error, NoSuchUniqueId unexpected here!", e);
throw e;
}
}
/**
* Extracts the value ID of the given tag UD name from the given row key.
* @param tsdb The TSDB instance to use for UniqueId lookups.
* @param row The row key in which to search the tag name.
* @param name The name of the tag to search in the row key.
* @return The value ID associated with the given tag ID, or null if this
* tag ID isn't present in this row key.
*/
static byte[] getValueId(final TSDB tsdb, final byte[] row,
final byte[] tag_id) {
final short name_width = tsdb.tag_names.width();
final short value_width = tsdb.tag_values.width();
// TODO(tsuna): Can do a binary search.
for (short pos = (short) (Const.SALT_WIDTH() +
tsdb.metrics.width() + Const.TIMESTAMP_BYTES);
pos < row.length;
pos += name_width + value_width) {
if (rowContains(row, pos, tag_id)) {
pos += name_width;
return Arrays.copyOfRange(row, pos, pos + value_width);
}
}
return null;
}
/**
* Checks whether or not the row key contains the given byte array at the
* given offset.
* @param row The row key in which to search.
* @param offset The offset in {@code row} at which to start searching.
* @param bytes The bytes to search that the given offset.
* @return true if {@code bytes} are present in {@code row} at
* {@code offset}, false otherwise.
*/
private static boolean rowContains(final byte[] row,
short offset, final byte[] bytes) {
for (int pos = bytes.length - 1; pos >= 0; pos--) {
if (row[offset + pos] != bytes[pos]) {
return false;
}
}
return true;
}
/**
* Returns the tags stored in the given row key.
* @param tsdb The TSDB instance to use for Unique ID lookups.
* @param row The row key from which to extract the tags.
* @return A map of tag names (keys), tag values (values).
* @throws NoSuchUniqueId if the row key contained an invalid ID (unlikely).
*/
static Map<String, String> getTags(final TSDB tsdb,
final byte[] row) throws NoSuchUniqueId {
try {
return getTagsAsync(tsdb, row).joinUninterruptibly();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Should never be here", e);
}
}
/**
* Returns the tags stored in the given row key.
* @param tsdb The TSDB instance to use for Unique ID lookups.
* @param row The row key from which to extract the tags.
* @return A map of tag names (keys), tag values (values).
* @throws NoSuchUniqueId if the row key contained an invalid ID (unlikely).
* @since 1.2
*/
static Deferred<Map<String, String>> getTagsAsync(final TSDB tsdb,
final byte[] row) throws NoSuchUniqueId {
final short name_width = tsdb.tag_names.width();
final short value_width = tsdb.tag_values.width();
final short tag_bytes = (short) (name_width + value_width);
final short metric_ts_bytes = (short) (Const.SALT_WIDTH()
+ tsdb.metrics.width()
+ Const.TIMESTAMP_BYTES);
final ArrayList<Deferred<String>> deferreds =
new ArrayList<Deferred<String>>((row.length - metric_ts_bytes) / tag_bytes);
for (short pos = metric_ts_bytes; pos < row.length; pos += tag_bytes) {
final byte[] tmp_name = new byte[name_width];
final byte[] tmp_value = new byte[value_width];
System.arraycopy(row, pos, tmp_name, 0, name_width);
deferreds.add(tsdb.tag_names.getNameAsync(tmp_name));
System.arraycopy(row, pos + name_width, tmp_value, 0, value_width);
deferreds.add(tsdb.tag_values.getNameAsync(tmp_value));
}
class NameCB implements Callback<Map<String, String>, ArrayList<String>> {
public Map<String, String> call(final ArrayList<String> names)
throws Exception {
final HashMap<String, String> result = new HashMap<String, String>(
(row.length - metric_ts_bytes) / tag_bytes);
String tagk = "";
for (String name : names) {
if (tagk.isEmpty()) {
tagk = name;
} else {
result.put(tagk, name);
tagk = "";
}
}
return result;
}
}
return Deferred.groupInOrder(deferreds).addCallback(new NameCB());
}
/**
* Returns the names mapped to tag key/value UIDs
* @param tsdb The TSDB instance to use for Unique ID lookups.
* @param tags The map of tag key to tag value pairs
* @return A map of tag names (keys), tag values (values). If the tags list
* was null or empty, the result will be an empty map
* @throws NoSuchUniqueId if the row key contained an invalid ID.
* @since 2.3
*/
public static Deferred<Map<String, String>> getTagsAsync(final TSDB tsdb,
final ByteMap<byte[]> tags) {
if (tags == null || tags.isEmpty()) {
return Deferred.fromResult(Collections.<String, String>emptyMap());
}
final ArrayList<Deferred<String>> deferreds =
new ArrayList<Deferred<String>>();
for (final Map.Entry<byte[], byte[]> pair : tags) {
deferreds.add(tsdb.tag_names.getNameAsync(pair.getKey()));
deferreds.add(tsdb.tag_values.getNameAsync(pair.getValue()));
}
class NameCB implements Callback<Map<String, String>, ArrayList<String>> {
public Map<String, String> call(final ArrayList<String> names)
throws Exception {
final HashMap<String, String> result = new HashMap<String, String>();
String tagk = "";
for (String name : names) {
if (tagk.isEmpty()) {
tagk = name;
} else {
result.put(tagk, name);
tagk = "";
}
}
return result;
}
}
return Deferred.groupInOrder(deferreds).addCallback(new NameCB());
}
/**
* Returns the tag key and value pairs as a byte map given a row key
* @param row The row key to parse the UIDs from
* @return A byte map with tagk and tagv pairs as raw UIDs
* @since 2.2
*/
public static ByteMap<byte[]> getTagUids(final byte[] row) {
final ByteMap<byte[]> uids = new ByteMap<byte[]>();
final short name_width = TSDB.tagk_width();
final short value_width = TSDB.tagv_width();
final short tag_bytes = (short) (name_width + value_width);
final short metric_ts_bytes = (short) (TSDB.metrics_width()
+ Const.TIMESTAMP_BYTES
+ Const.SALT_WIDTH());
for (short pos = metric_ts_bytes; pos < row.length; pos += tag_bytes) {
final byte[] tmp_name = new byte[name_width];
final byte[] tmp_value = new byte[value_width];
System.arraycopy(row, pos, tmp_name, 0, name_width);
System.arraycopy(row, pos + name_width, tmp_value, 0, value_width);
uids.put(tmp_name, tmp_value);
}
return uids;
}
/**
* Ensures that a given string is a valid metric name or tag name/value.
* @param what A human readable description of what's being validated.
* @param s The string to validate.
* @throws IllegalArgumentException if the string isn't valid.
*/
public static void validateString(final String what, final String s) {
if (s == null) {
throw new IllegalArgumentException("Invalid " + what + ": null");
} else if ("".equals(s)) {
throw new IllegalArgumentException("Invalid " + what + ": empty string");
}
final int n = s.length();
for (int i = 0; i < n; i++) {
final char c = s.charAt(i);
if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
|| ('0' <= c && c <= '9') || c == '-' || c == '_' || c == '.'
|| c == '/' || Character.isLetter(c) || isAllowSpecialChars(c))) {
throw new IllegalArgumentException("Invalid " + what
+ " (\"" + s + "\"): illegal character: " + c);
}
}
}
/**
* Resolves all the tags (name=value) into the a sorted byte arrays.
* This function is the opposite of {@link #resolveIds}.
* @param tsdb The TSDB to use for UniqueId lookups.
* @param tags The tags to resolve.
* @return an array of sorted tags (tag id, tag name).
* @throws NoSuchUniqueName if one of the elements in the map contained an
* unknown tag name or tag value.
*/
public static ArrayList<byte[]> resolveAll(final TSDB tsdb,
final Map<String, String> tags)
throws NoSuchUniqueName {
try {
return resolveAllInternal(tsdb, tags, false);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Should never happen!", e);
}
}
/**
* Resolves a set of tag strings to their UIDs asynchronously
* @param tsdb the TSDB to use for access
* @param tags The tags to resolve
* @return A deferred with the list of UIDs in tagk1, tagv1, .. tagkn, tagvn
* order
* @throws NoSuchUniqueName if one of the elements in the map contained an
* unknown tag name or tag value.
* @since 2.1
*/
public static Deferred<ArrayList<byte[]>> resolveAllAsync(final TSDB tsdb,
final Map<String, String> tags) {
return resolveAllInternalAsync(tsdb, null, tags, false);
}
/**
* Resolves (and creates, if necessary) all the tags (name=value) into the a
* sorted byte arrays.
* @param tsdb The TSDB to use for UniqueId lookups.
* @param tags The tags to resolve. If a new tag name or tag value is
* seen, it will be assigned an ID.
* @return an array of sorted tags (tag id, tag name).
*/
static ArrayList<byte[]> resolveOrCreateAll(final TSDB tsdb,
final Map<String, String> tags) {
return resolveAllInternal(tsdb, tags, true);
}
private
static ArrayList<byte[]> resolveAllInternal(final TSDB tsdb,
final Map<String, String> tags,
final boolean create)
throws NoSuchUniqueName {
final ArrayList<byte[]> tag_ids = new ArrayList<byte[]>(tags.size());
for (final Map.Entry<String, String> entry : tags.entrySet()) {
final byte[] tag_id = (create && tsdb.getConfig().auto_tagk()
? tsdb.tag_names.getOrCreateId(entry.getKey())
: tsdb.tag_names.getId(entry.getKey()));
final byte[] value_id = (create && tsdb.getConfig().auto_tagv()
? tsdb.tag_values.getOrCreateId(entry.getValue())
: tsdb.tag_values.getId(entry.getValue()));
final byte[] thistag = new byte[tag_id.length + value_id.length];
System.arraycopy(tag_id, 0, thistag, 0, tag_id.length);
System.arraycopy(value_id, 0, thistag, tag_id.length, value_id.length);
tag_ids.add(thistag);
}
// Now sort the tags.
Collections.sort(tag_ids, Bytes.MEMCMP);
return tag_ids;
}
/**
* Resolves (and creates, if necessary) all the tags (name=value) into the a
* sorted byte arrays.
* @param tsdb The TSDB to use for UniqueId lookups.
* @param tags The tags to resolve. If a new tag name or tag value is
* seen, it will be assigned an ID.
* @return an array of sorted tags (tag id, tag name).
* @since 2.0
*/
static Deferred<ArrayList<byte[]>>
resolveOrCreateAllAsync(final TSDB tsdb, final Map<String, String> tags) {
return resolveAllInternalAsync(tsdb, null, tags, true);
}
/**
* Resolves (and creates, if necessary) all the tags (name=value) into the a
* sorted byte arrays.
* @param tsdb The TSDB to use for UniqueId lookups.
* @param metric The metric associated with this tag set for filtering.
* @param tags The tags to resolve. If a new tag name or tag value is
* seen, it will be assigned an ID.
* @return an array of sorted tags (tag id, tag name).
* @since 2.3
*/
static Deferred<ArrayList<byte[]>>
resolveOrCreateAllAsync(final TSDB tsdb, final String metric,
final Map<String, String> tags) {
return resolveAllInternalAsync(tsdb, metric, tags, true);
}
private static Deferred<ArrayList<byte[]>>
resolveAllInternalAsync(final TSDB tsdb,
final String metric,
final Map<String, String> tags,
final boolean create) {
final ArrayList<Deferred<byte[]>> tag_ids =
new ArrayList<Deferred<byte[]>>(tags.size());
// For each tag, start resolving the tag name and the tag value.
for (final Map.Entry<String, String> entry : tags.entrySet()) {
final Deferred<byte[]> name_id = create
? tsdb.tag_names.getOrCreateIdAsync(entry.getKey(), metric, tags)
: tsdb.tag_names.getIdAsync(entry.getKey());
final Deferred<byte[]> value_id = create
? tsdb.tag_values.getOrCreateIdAsync(entry.getValue(), metric, tags)
: tsdb.tag_values.getIdAsync(entry.getValue());
// Then once the tag name is resolved, get the resolved tag value.
class TagNameResolvedCB implements Callback<Deferred<byte[]>, byte[]> {
public Deferred<byte[]> call(final byte[] nameid) {
// And once the tag value too is resolved, paste the two together.
class TagValueResolvedCB implements Callback<byte[], byte[]> {
public byte[] call(final byte[] valueid) {
final byte[] thistag = new byte[nameid.length + valueid.length];
System.arraycopy(nameid, 0, thistag, 0, nameid.length);
System.arraycopy(valueid, 0, thistag, nameid.length, valueid.length);
return thistag;
}
}
return value_id.addCallback(new TagValueResolvedCB());
}
}
// Put all the deferred tag resolutions in this list.
final Deferred<byte[]> resolve =
name_id.addCallbackDeferring(new TagNameResolvedCB());
tag_ids.add(resolve);
}
// And then once we have all the tags resolved, sort them.
return Deferred.group(tag_ids).addCallback(SORT_CB);
}
/**
* Sorts a list of tags.
* Each entry in the list expected to be a byte array that contains the tag
* name UID followed by the tag value UID.
*/
private static class SortResolvedTagsCB
implements Callback<ArrayList<byte[]>, ArrayList<byte[]>> {
public ArrayList<byte[]> call(final ArrayList<byte[]> tags) {
// Now sort the tags.
Collections.sort(tags, Bytes.MEMCMP);
return tags;
}
}
private static final SortResolvedTagsCB SORT_CB = new SortResolvedTagsCB();
/**
* Resolves all the tags IDs (name followed by value) into the a map.
* This function is the opposite of {@link #resolveAll}.
* @param tsdb The TSDB to use for UniqueId lookups.
* @param tags The tag IDs to resolve.
* @return A map mapping tag names to tag values.
* @throws NoSuchUniqueId if one of the elements in the array contained an
* invalid ID.
* @throws IllegalArgumentException if one of the elements in the array had
* the wrong number of bytes.
*/
public static HashMap<String, String> resolveIds(final TSDB tsdb,
final ArrayList<byte[]> tags)
throws NoSuchUniqueId {
try {
return resolveIdsAsync(tsdb, tags).joinUninterruptibly();
} catch (NoSuchUniqueId e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Shouldn't be here", e);
}
}
/**
* Resolves all the tags IDs asynchronously (name followed by value) into a map.
* This function is the opposite of {@link #resolveAll}.
* @param tsdb The TSDB to use for UniqueId lookups.
* @param tags The tag IDs to resolve.
* @return A map mapping tag names to tag values.
* @throws NoSuchUniqueId if one of the elements in the array contained an
* invalid ID.
* @throws IllegalArgumentException if one of the elements in the array had
* the wrong number of bytes.
* @since 2.0
*/
public static Deferred<HashMap<String, String>> resolveIdsAsync(final TSDB tsdb,
final List<byte[]> tags)
throws NoSuchUniqueId {
final short name_width = tsdb.tag_names.width();
final short value_width = tsdb.tag_values.width();
final short tag_bytes = (short) (name_width + value_width);
final HashMap<String, String> result
= new HashMap<String, String>(tags.size());
final ArrayList<Deferred<String>> deferreds
= new ArrayList<Deferred<String>>(tags.size());
for (final byte[] tag : tags) {
final byte[] tmp_name = new byte[name_width];
final byte[] tmp_value = new byte[value_width];
if (tag.length != tag_bytes) {
throw new IllegalArgumentException("invalid length: " + tag.length
+ " (expected " + tag_bytes + "): " + Arrays.toString(tag));
}
System.arraycopy(tag, 0, tmp_name, 0, name_width);
deferreds.add(tsdb.tag_names.getNameAsync(tmp_name));
System.arraycopy(tag, name_width, tmp_value, 0, value_width);
deferreds.add(tsdb.tag_values.getNameAsync(tmp_value));
}
class GroupCB implements Callback<HashMap<String, String>, ArrayList<String>> {
public HashMap<String, String> call(final ArrayList<String> names)
throws Exception {
for (int i = 0; i < names.size(); i++) {
if (i % 2 != 0) {
result.put(names.get(i - 1), names.get(i));
}
}
return result;
}
}
return Deferred.groupInOrder(deferreds).addCallback(new GroupCB());
}
/**
* Returns true if the given string looks like an integer.
* <p>
* This function doesn't do any checking on the string other than looking
* for some characters that are generally found in floating point values
* such as '.' or 'e'.
* @since 1.1
*/
public static boolean looksLikeInteger(final String value) {
final int n = value.length();
for (int i = 0; i < n; i++) {
final char c = value.charAt(i);
if (c == '.' || c == 'e' || c == 'E') {
return false;
}
}
return true;
}
/**
* Set the special characters due to allowing for a key or a value of the tag.
* @param characters character sequences as a string
*/
public static void setAllowSpecialChars(String characters) {
allowSpecialChars = characters == null ? "" : characters;
}
/**
* Returns true if the character can be used a tag name or a tag value.
* @param character
* @return
*/
static boolean isAllowSpecialChars(char character) {
return allowSpecialChars.indexOf(character) != -1;
}
}