1 // This file is part of OpenTSDB. 2 // Copyright (C) 2010-2012 The OpenTSDB Authors. 3 // 4 // This program is free software: you can redistribute it and/or modify it 5 // under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 2.1 of the License, or (at your 7 // option) any later version. This program is distributed in the hope that it 8 // will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License for more details. You should have received a copy 11 // of the GNU Lesser General Public License along with this program. If not, 12 // see <http://www.gnu.org/licenses/>. 13 package net.opentsdb.core; 14 15 import java.util.Arrays; 16 import java.util.ArrayList; 17 import java.util.Collections; 18 import java.util.HashMap; 19 import java.util.List; 20 import java.util.Map; 21 22 import org.slf4j.Logger; 23 import org.slf4j.LoggerFactory; 24 25 import com.stumbleupon.async.Callback; 26 import com.stumbleupon.async.Deferred; 27 28 import org.hbase.async.Bytes; 29 import org.hbase.async.Bytes.ByteMap; 30 31 import net.opentsdb.query.filter.TagVFilter; 32 import net.opentsdb.uid.NoSuchUniqueId; 33 import net.opentsdb.uid.NoSuchUniqueName; 34 import net.opentsdb.utils.Pair; 35 36 /** Helper functions to deal with tags. */ 37 public final class Tags { 38 39 private static final Logger LOG = LoggerFactory.getLogger(Tags.class); 40 private static String allowSpecialChars = ""; 41 Tags()42 private Tags() { 43 // Can't create instances of this utility class. 44 } 45 46 /** 47 * Optimized version of {@code String#split} that doesn't use regexps. 48 * This function works in O(5n) where n is the length of the string to 49 * split. 50 * @param s The string to split. 51 * @param c The separator to use to split the string. 52 * @return A non-null, non-empty array. 53 */ splitString(final String s, final char c)54 public static String[] splitString(final String s, final char c) { 55 final char[] chars = s.toCharArray(); 56 int num_substrings = 1; 57 for (final char x : chars) { 58 if (x == c) { 59 num_substrings++; 60 } 61 } 62 final String[] result = new String[num_substrings]; 63 final int len = chars.length; 64 int start = 0; // starting index in chars of the current substring. 65 int pos = 0; // current index in chars. 66 int i = 0; // number of the current substring. 67 for (; pos < len; pos++) { 68 if (chars[pos] == c) { 69 result[i++] = new String(chars, start, pos - start); 70 start = pos + 1; 71 } 72 } 73 result[i] = new String(chars, start, pos - start); 74 return result; 75 } 76 77 /** 78 * Parses a tag into a HashMap. 79 * @param tags The HashMap into which to store the tag. 80 * @param tag A String of the form "tag=value". 81 * @throws IllegalArgumentException if the tag is malformed. 82 * @throws IllegalArgumentException if the tag was already in tags with a 83 * different value. 84 */ parse(final HashMap<String, String> tags, final String tag)85 public static void parse(final HashMap<String, String> tags, 86 final String tag) { 87 final String[] kv = splitString(tag, '='); 88 if (kv.length != 2 || kv[0].length() <= 0 || kv[1].length() <= 0) { 89 throw new IllegalArgumentException("invalid tag: " + tag); 90 } 91 if (kv[1].equals(tags.get(kv[0]))) { 92 return; 93 } 94 if (tags.get(kv[0]) != null) { 95 throw new IllegalArgumentException("duplicate tag: " + tag 96 + ", tags=" + tags); 97 } 98 tags.put(kv[0], kv[1]); 99 } 100 101 /** 102 * Parses a tag into a list of key/value pairs, allowing nulls for either 103 * value. 104 * @param tags The list into which the parsed tag should be stored 105 * @param tag A string of the form "tag=value" or "=value" or "tag=" 106 * @throws IllegalArgumentException if the tag is malformed. 107 * @since 2.1 108 */ parse(final List<Pair<String, String>> tags, final String tag)109 public static void parse(final List<Pair<String, String>> tags, 110 final String tag) { 111 if (tag == null || tag.isEmpty() || tag.length() < 2) { 112 throw new IllegalArgumentException("Missing tag pair"); 113 } 114 if (tag.charAt(0) == '=') { 115 tags.add(new Pair<String, String>(null, tag.substring(1))); 116 return; 117 } else if (tag.charAt(tag.length() - 1) == '=') { 118 tags.add(new Pair<String, String>(tag.substring(0, tag.length() - 1), null)); 119 return; 120 } 121 122 final String[] kv = splitString(tag, '='); 123 if (kv.length != 2 || kv[0].length() <= 0 || kv[1].length() <= 0) { 124 throw new IllegalArgumentException("invalid tag: " + tag); 125 } 126 tags.add(new Pair<String, String>(kv[0], kv[1])); 127 } 128 129 /** 130 * Parses the metric and tags out of the given string. 131 * @param metric A string of the form "metric" or "metric{tag=value,...}". 132 * @param tags The map to populate with the tags parsed out of the first 133 * argument. 134 * @return The name of the metric. 135 * @throws IllegalArgumentException if the metric is malformed. 136 */ parseWithMetric(final String metric, final HashMap<String, String> tags)137 public static String parseWithMetric(final String metric, 138 final HashMap<String, String> tags) { 139 final int curly = metric.indexOf('{'); 140 if (curly < 0) { 141 return metric; 142 } 143 final int len = metric.length(); 144 if (metric.charAt(len - 1) != '}') { // "foo{" 145 throw new IllegalArgumentException("Missing '}' at the end of: " + metric); 146 } else if (curly == len - 2) { // "foo{}" 147 return metric.substring(0, len - 2); 148 } 149 // substring the tags out of "foo{a=b,...,x=y}" and parse them. 150 for (final String tag : splitString(metric.substring(curly + 1, len - 1), 151 ',')) { 152 try { 153 parse(tags, tag); 154 } catch (IllegalArgumentException e) { 155 throw new IllegalArgumentException("When parsing tag '" + tag 156 + "': " + e.getMessage()); 157 } 158 } 159 // Return the "foo" part of "foo{a=b,...,x=y}" 160 return metric.substring(0, curly); 161 } 162 163 /** 164 * Parses an optional metric and tags out of the given string, any of 165 * which may be null. Requires at least one metric, tagk or tagv. 166 * @param metric A string of the form "metric" or "metric{tag=value,...}" 167 * or even "{tag=value,...}" where the metric may be missing. 168 * @param tags The list to populate with parsed tag pairs 169 * @return The name of the metric if it exists, null otherwise 170 * @throws IllegalArgumentException if the metric is malformed. 171 * @since 2.1 172 */ parseWithMetric(final String metric, final List<Pair<String, String>> tags)173 public static String parseWithMetric(final String metric, 174 final List<Pair<String, String>> tags) { 175 final int curly = metric.indexOf('{'); 176 if (curly < 0) { 177 if (metric.isEmpty()) { 178 throw new IllegalArgumentException("Metric string was empty"); 179 } 180 return metric; 181 } 182 final int len = metric.length(); 183 if (metric.charAt(len - 1) != '}') { // "foo{" 184 throw new IllegalArgumentException("Missing '}' at the end of: " + metric); 185 } else if (curly == len - 2) { // "foo{}" 186 if (metric.charAt(0) == '{') { 187 throw new IllegalArgumentException("Missing metric and tags: " + metric); 188 } 189 return metric.substring(0, len - 2); 190 } 191 // substring the tags out of "foo{a=b,...,x=y}" and parse them. 192 for (final String tag : splitString(metric.substring(curly + 1, len - 1), 193 ',')) { 194 try { 195 parse(tags, tag); 196 } catch (IllegalArgumentException e) { 197 throw new IllegalArgumentException("When parsing tag '" + tag 198 + "': " + e.getMessage()); 199 } 200 } 201 // Return the "foo" part of "foo{a=b,...,x=y}" 202 if (metric.charAt(0) == '{') { 203 return null; 204 } 205 return metric.substring(0, curly); 206 } 207 208 /** 209 * Parses the metric and tags out of the given string. 210 * @param metric A string of the form "metric" or "metric{tag=value,...}" or 211 * now "metric{groupby=filter}{filter=filter}". 212 * @param filters A list of filters to write the results to. May not be null 213 * @return The name of the metric. 214 * @throws IllegalArgumentException if the metric is malformed or the filter 215 * list is null. 216 * @since 2.2 217 */ parseWithMetricAndFilters(final String metric, final List<TagVFilter> filters)218 public static String parseWithMetricAndFilters(final String metric, 219 final List<TagVFilter> filters) { 220 if (metric == null || metric.isEmpty()) { 221 throw new IllegalArgumentException("Metric cannot be null or empty"); 222 } 223 if (filters == null) { 224 throw new IllegalArgumentException("Filters cannot be null"); 225 } 226 final int curly = metric.indexOf('{'); 227 if (curly < 0) { 228 return metric; 229 } 230 final int len = metric.length(); 231 if (metric.charAt(len - 1) != '}') { // "foo{" 232 throw new IllegalArgumentException("Missing '}' at the end of: " + metric); 233 } else if (curly == len - 2) { // "foo{}" 234 return metric.substring(0, len - 2); 235 } 236 final int close = metric.indexOf('}'); 237 final HashMap<String, String> filter_map = new HashMap<String, String>(); 238 if (close != metric.length() - 1) { // "foo{...}{tagk=filter}" 239 final int filter_bracket = metric.lastIndexOf('{'); 240 for (final String filter : splitString(metric.substring(filter_bracket + 1, 241 metric.length() - 1), ',')) { 242 if (filter.isEmpty()) { 243 break; 244 } 245 filter_map.clear(); 246 try { 247 parse(filter_map, filter); 248 TagVFilter.mapToFilters(filter_map, filters, false); 249 } catch (IllegalArgumentException e) { 250 throw new IllegalArgumentException("When parsing filter '" + filter 251 + "': " + e.getMessage(), e); 252 } 253 } 254 } 255 256 // substring the tags out of "foo{a=b,...,x=y}" and parse them. 257 for (final String tag : splitString(metric.substring(curly + 1, close), ',')) { 258 try { 259 if (tag.isEmpty() && close != metric.length() - 1){ 260 break; 261 } 262 filter_map.clear(); 263 parse(filter_map, tag); 264 TagVFilter.tagsToFilters(filter_map, filters); 265 } catch (IllegalArgumentException e) { 266 throw new IllegalArgumentException("When parsing tag '" + tag 267 + "': " + e.getMessage(), e); 268 } 269 } 270 // Return the "foo" part of "foo{a=b,...,x=y}" 271 return metric.substring(0, curly); 272 } 273 274 /** 275 * Parses an integer value as a long from the given character sequence. 276 * <p> 277 * This is equivalent to {@link Long#parseLong(String)} except it's up to 278 * 100% faster on {@link String} and always works in O(1) space even with 279 * {@link StringBuilder} buffers (where it's 2x to 5x faster). 280 * @param s The character sequence containing the integer value to parse. 281 * @return The value parsed. 282 * @throws NumberFormatException if the value is malformed or overflows. 283 */ parseLong(final CharSequence s)284 public static long parseLong(final CharSequence s) { 285 final int n = s.length(); // Will NPE if necessary. 286 if (n == 0) { 287 throw new NumberFormatException("Empty string"); 288 } 289 char c = s.charAt(0); // Current character. 290 int i = 1; // index in `s'. 291 if (c < '0' && (c == '+' || c == '-')) { // Only 1 test in common case. 292 if (n == 1) { 293 throw new NumberFormatException("Just a sign, no value: " + s); 294 } else if (n > 20) { // "+9223372036854775807" or "-9223372036854775808" 295 throw new NumberFormatException("Value too long: " + s); 296 } 297 c = s.charAt(1); 298 i = 2; // Skip over the sign. 299 } else if (n > 19) { // "9223372036854775807" 300 throw new NumberFormatException("Value too long: " + s); 301 } 302 long v = 0; // The result (negated to easily handle MIN_VALUE). 303 do { 304 if ('0' <= c && c <= '9') { 305 v -= c - '0'; 306 } else { 307 throw new NumberFormatException("Invalid character '" + c 308 + "' in " + s); 309 } 310 if (i == n) { 311 break; 312 } 313 v *= 10; 314 c = s.charAt(i++); 315 } while (true); 316 if (v > 0) { 317 throw new NumberFormatException("Overflow in " + s); 318 } else if (s.charAt(0) == '-') { 319 return v; // Value is already negative, return unchanged. 320 } else if (v == Long.MIN_VALUE) { 321 throw new NumberFormatException("Overflow in " + s); 322 } else { 323 return -v; // Positive value, need to fix the sign. 324 } 325 } 326 327 /** 328 * Extracts the value of the given tag name from the given row key. 329 * @param tsdb The TSDB instance to use for UniqueId lookups. 330 * @param row The row key in which to search the tag name. 331 * @param name The name of the tag to search in the row key. 332 * @return The value associated with the given tag name, or null if this tag 333 * isn't present in this row key. 334 */ getValue(final TSDB tsdb, final byte[] row, final String name)335 static String getValue(final TSDB tsdb, final byte[] row, 336 final String name) throws NoSuchUniqueName { 337 validateString("tag name", name); 338 final byte[] id = tsdb.tag_names.getId(name); 339 final byte[] value_id = getValueId(tsdb, row, id); 340 if (value_id == null) { 341 return null; 342 } 343 // This shouldn't throw a NoSuchUniqueId. 344 try { 345 return tsdb.tag_values.getName(value_id); 346 } catch (NoSuchUniqueId e) { 347 LOG.error("Internal error, NoSuchUniqueId unexpected here!", e); 348 throw e; 349 } 350 } 351 352 /** 353 * Extracts the value ID of the given tag UD name from the given row key. 354 * @param tsdb The TSDB instance to use for UniqueId lookups. 355 * @param row The row key in which to search the tag name. 356 * @param name The name of the tag to search in the row key. 357 * @return The value ID associated with the given tag ID, or null if this 358 * tag ID isn't present in this row key. 359 */ getValueId(final TSDB tsdb, final byte[] row, final byte[] tag_id)360 static byte[] getValueId(final TSDB tsdb, final byte[] row, 361 final byte[] tag_id) { 362 final short name_width = tsdb.tag_names.width(); 363 final short value_width = tsdb.tag_values.width(); 364 // TODO(tsuna): Can do a binary search. 365 for (short pos = (short) (Const.SALT_WIDTH() + 366 tsdb.metrics.width() + Const.TIMESTAMP_BYTES); 367 pos < row.length; 368 pos += name_width + value_width) { 369 if (rowContains(row, pos, tag_id)) { 370 pos += name_width; 371 return Arrays.copyOfRange(row, pos, pos + value_width); 372 } 373 } 374 return null; 375 } 376 377 /** 378 * Checks whether or not the row key contains the given byte array at the 379 * given offset. 380 * @param row The row key in which to search. 381 * @param offset The offset in {@code row} at which to start searching. 382 * @param bytes The bytes to search that the given offset. 383 * @return true if {@code bytes} are present in {@code row} at 384 * {@code offset}, false otherwise. 385 */ rowContains(final byte[] row, short offset, final byte[] bytes)386 private static boolean rowContains(final byte[] row, 387 short offset, final byte[] bytes) { 388 for (int pos = bytes.length - 1; pos >= 0; pos--) { 389 if (row[offset + pos] != bytes[pos]) { 390 return false; 391 } 392 } 393 return true; 394 } 395 396 /** 397 * Returns the tags stored in the given row key. 398 * @param tsdb The TSDB instance to use for Unique ID lookups. 399 * @param row The row key from which to extract the tags. 400 * @return A map of tag names (keys), tag values (values). 401 * @throws NoSuchUniqueId if the row key contained an invalid ID (unlikely). 402 */ getTags(final TSDB tsdb, final byte[] row)403 static Map<String, String> getTags(final TSDB tsdb, 404 final byte[] row) throws NoSuchUniqueId { 405 try { 406 return getTagsAsync(tsdb, row).joinUninterruptibly(); 407 } catch (RuntimeException e) { 408 throw e; 409 } catch (Exception e) { 410 throw new RuntimeException("Should never be here", e); 411 } 412 } 413 414 /** 415 * Returns the tags stored in the given row key. 416 * @param tsdb The TSDB instance to use for Unique ID lookups. 417 * @param row The row key from which to extract the tags. 418 * @return A map of tag names (keys), tag values (values). 419 * @throws NoSuchUniqueId if the row key contained an invalid ID (unlikely). 420 * @since 1.2 421 */ getTagsAsync(final TSDB tsdb, final byte[] row)422 static Deferred<Map<String, String>> getTagsAsync(final TSDB tsdb, 423 final byte[] row) throws NoSuchUniqueId { 424 final short name_width = tsdb.tag_names.width(); 425 final short value_width = tsdb.tag_values.width(); 426 final short tag_bytes = (short) (name_width + value_width); 427 final short metric_ts_bytes = (short) (Const.SALT_WIDTH() 428 + tsdb.metrics.width() 429 + Const.TIMESTAMP_BYTES); 430 431 final ArrayList<Deferred<String>> deferreds = 432 new ArrayList<Deferred<String>>((row.length - metric_ts_bytes) / tag_bytes); 433 434 for (short pos = metric_ts_bytes; pos < row.length; pos += tag_bytes) { 435 final byte[] tmp_name = new byte[name_width]; 436 final byte[] tmp_value = new byte[value_width]; 437 438 System.arraycopy(row, pos, tmp_name, 0, name_width); 439 deferreds.add(tsdb.tag_names.getNameAsync(tmp_name)); 440 441 System.arraycopy(row, pos + name_width, tmp_value, 0, value_width); 442 deferreds.add(tsdb.tag_values.getNameAsync(tmp_value)); 443 } 444 445 class NameCB implements Callback<Map<String, String>, ArrayList<String>> { 446 public Map<String, String> call(final ArrayList<String> names) 447 throws Exception { 448 final HashMap<String, String> result = new HashMap<String, String>( 449 (row.length - metric_ts_bytes) / tag_bytes); 450 String tagk = ""; 451 for (String name : names) { 452 if (tagk.isEmpty()) { 453 tagk = name; 454 } else { 455 result.put(tagk, name); 456 tagk = ""; 457 } 458 } 459 return result; 460 } 461 } 462 463 return Deferred.groupInOrder(deferreds).addCallback(new NameCB()); 464 } 465 466 /** 467 * Returns the names mapped to tag key/value UIDs 468 * @param tsdb The TSDB instance to use for Unique ID lookups. 469 * @param tags The map of tag key to tag value pairs 470 * @return A map of tag names (keys), tag values (values). If the tags list 471 * was null or empty, the result will be an empty map 472 * @throws NoSuchUniqueId if the row key contained an invalid ID. 473 * @since 2.3 474 */ getTagsAsync(final TSDB tsdb, final ByteMap<byte[]> tags)475 public static Deferred<Map<String, String>> getTagsAsync(final TSDB tsdb, 476 final ByteMap<byte[]> tags) { 477 if (tags == null || tags.isEmpty()) { 478 return Deferred.fromResult(Collections.<String, String>emptyMap()); 479 } 480 481 final ArrayList<Deferred<String>> deferreds = 482 new ArrayList<Deferred<String>>(); 483 484 for (final Map.Entry<byte[], byte[]> pair : tags) { 485 deferreds.add(tsdb.tag_names.getNameAsync(pair.getKey())); 486 deferreds.add(tsdb.tag_values.getNameAsync(pair.getValue())); 487 } 488 489 class NameCB implements Callback<Map<String, String>, ArrayList<String>> { 490 public Map<String, String> call(final ArrayList<String> names) 491 throws Exception { 492 final HashMap<String, String> result = new HashMap<String, String>(); 493 String tagk = ""; 494 for (String name : names) { 495 if (tagk.isEmpty()) { 496 tagk = name; 497 } else { 498 result.put(tagk, name); 499 tagk = ""; 500 } 501 } 502 return result; 503 } 504 } 505 506 return Deferred.groupInOrder(deferreds).addCallback(new NameCB()); 507 } 508 509 /** 510 * Returns the tag key and value pairs as a byte map given a row key 511 * @param row The row key to parse the UIDs from 512 * @return A byte map with tagk and tagv pairs as raw UIDs 513 * @since 2.2 514 */ getTagUids(final byte[] row)515 public static ByteMap<byte[]> getTagUids(final byte[] row) { 516 final ByteMap<byte[]> uids = new ByteMap<byte[]>(); 517 final short name_width = TSDB.tagk_width(); 518 final short value_width = TSDB.tagv_width(); 519 final short tag_bytes = (short) (name_width + value_width); 520 final short metric_ts_bytes = (short) (TSDB.metrics_width() 521 + Const.TIMESTAMP_BYTES 522 + Const.SALT_WIDTH()); 523 524 for (short pos = metric_ts_bytes; pos < row.length; pos += tag_bytes) { 525 final byte[] tmp_name = new byte[name_width]; 526 final byte[] tmp_value = new byte[value_width]; 527 System.arraycopy(row, pos, tmp_name, 0, name_width); 528 System.arraycopy(row, pos + name_width, tmp_value, 0, value_width); 529 uids.put(tmp_name, tmp_value); 530 } 531 return uids; 532 } 533 534 /** 535 * Ensures that a given string is a valid metric name or tag name/value. 536 * @param what A human readable description of what's being validated. 537 * @param s The string to validate. 538 * @throws IllegalArgumentException if the string isn't valid. 539 */ validateString(final String what, final String s)540 public static void validateString(final String what, final String s) { 541 if (s == null) { 542 throw new IllegalArgumentException("Invalid " + what + ": null"); 543 } else if ("".equals(s)) { 544 throw new IllegalArgumentException("Invalid " + what + ": empty string"); 545 } 546 final int n = s.length(); 547 for (int i = 0; i < n; i++) { 548 final char c = s.charAt(i); 549 if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') 550 || ('0' <= c && c <= '9') || c == '-' || c == '_' || c == '.' 551 || c == '/' || Character.isLetter(c) || isAllowSpecialChars(c))) { 552 throw new IllegalArgumentException("Invalid " + what 553 + " (\"" + s + "\"): illegal character: " + c); 554 } 555 } 556 } 557 558 /** 559 * Resolves all the tags (name=value) into the a sorted byte arrays. 560 * This function is the opposite of {@link #resolveIds}. 561 * @param tsdb The TSDB to use for UniqueId lookups. 562 * @param tags The tags to resolve. 563 * @return an array of sorted tags (tag id, tag name). 564 * @throws NoSuchUniqueName if one of the elements in the map contained an 565 * unknown tag name or tag value. 566 */ resolveAll(final TSDB tsdb, final Map<String, String> tags)567 public static ArrayList<byte[]> resolveAll(final TSDB tsdb, 568 final Map<String, String> tags) 569 throws NoSuchUniqueName { 570 try { 571 return resolveAllInternal(tsdb, tags, false); 572 } catch (RuntimeException e) { 573 throw e; 574 } catch (Exception e) { 575 throw new RuntimeException("Should never happen!", e); 576 } 577 } 578 579 /** 580 * Resolves a set of tag strings to their UIDs asynchronously 581 * @param tsdb the TSDB to use for access 582 * @param tags The tags to resolve 583 * @return A deferred with the list of UIDs in tagk1, tagv1, .. tagkn, tagvn 584 * order 585 * @throws NoSuchUniqueName if one of the elements in the map contained an 586 * unknown tag name or tag value. 587 * @since 2.1 588 */ resolveAllAsync(final TSDB tsdb, final Map<String, String> tags)589 public static Deferred<ArrayList<byte[]>> resolveAllAsync(final TSDB tsdb, 590 final Map<String, String> tags) { 591 return resolveAllInternalAsync(tsdb, null, tags, false); 592 } 593 594 /** 595 * Resolves (and creates, if necessary) all the tags (name=value) into the a 596 * sorted byte arrays. 597 * @param tsdb The TSDB to use for UniqueId lookups. 598 * @param tags The tags to resolve. If a new tag name or tag value is 599 * seen, it will be assigned an ID. 600 * @return an array of sorted tags (tag id, tag name). 601 */ resolveOrCreateAll(final TSDB tsdb, final Map<String, String> tags)602 static ArrayList<byte[]> resolveOrCreateAll(final TSDB tsdb, 603 final Map<String, String> tags) { 604 return resolveAllInternal(tsdb, tags, true); 605 } 606 607 private resolveAllInternal(final TSDB tsdb, final Map<String, String> tags, final boolean create)608 static ArrayList<byte[]> resolveAllInternal(final TSDB tsdb, 609 final Map<String, String> tags, 610 final boolean create) 611 throws NoSuchUniqueName { 612 final ArrayList<byte[]> tag_ids = new ArrayList<byte[]>(tags.size()); 613 for (final Map.Entry<String, String> entry : tags.entrySet()) { 614 final byte[] tag_id = (create && tsdb.getConfig().auto_tagk() 615 ? tsdb.tag_names.getOrCreateId(entry.getKey()) 616 : tsdb.tag_names.getId(entry.getKey())); 617 final byte[] value_id = (create && tsdb.getConfig().auto_tagv() 618 ? tsdb.tag_values.getOrCreateId(entry.getValue()) 619 : tsdb.tag_values.getId(entry.getValue())); 620 final byte[] thistag = new byte[tag_id.length + value_id.length]; 621 System.arraycopy(tag_id, 0, thistag, 0, tag_id.length); 622 System.arraycopy(value_id, 0, thistag, tag_id.length, value_id.length); 623 tag_ids.add(thistag); 624 } 625 // Now sort the tags. 626 Collections.sort(tag_ids, Bytes.MEMCMP); 627 return tag_ids; 628 } 629 630 /** 631 * Resolves (and creates, if necessary) all the tags (name=value) into the a 632 * sorted byte arrays. 633 * @param tsdb The TSDB to use for UniqueId lookups. 634 * @param tags The tags to resolve. If a new tag name or tag value is 635 * seen, it will be assigned an ID. 636 * @return an array of sorted tags (tag id, tag name). 637 * @since 2.0 638 */ 639 static Deferred<ArrayList<byte[]>> resolveOrCreateAllAsync(final TSDB tsdb, final Map<String, String> tags)640 resolveOrCreateAllAsync(final TSDB tsdb, final Map<String, String> tags) { 641 return resolveAllInternalAsync(tsdb, null, tags, true); 642 } 643 644 /** 645 * Resolves (and creates, if necessary) all the tags (name=value) into the a 646 * sorted byte arrays. 647 * @param tsdb The TSDB to use for UniqueId lookups. 648 * @param metric The metric associated with this tag set for filtering. 649 * @param tags The tags to resolve. If a new tag name or tag value is 650 * seen, it will be assigned an ID. 651 * @return an array of sorted tags (tag id, tag name). 652 * @since 2.3 653 */ 654 static Deferred<ArrayList<byte[]>> resolveOrCreateAllAsync(final TSDB tsdb, final String metric, final Map<String, String> tags)655 resolveOrCreateAllAsync(final TSDB tsdb, final String metric, 656 final Map<String, String> tags) { 657 return resolveAllInternalAsync(tsdb, metric, tags, true); 658 } 659 660 private static Deferred<ArrayList<byte[]>> resolveAllInternalAsync(final TSDB tsdb, final String metric, final Map<String, String> tags, final boolean create)661 resolveAllInternalAsync(final TSDB tsdb, 662 final String metric, 663 final Map<String, String> tags, 664 final boolean create) { 665 final ArrayList<Deferred<byte[]>> tag_ids = 666 new ArrayList<Deferred<byte[]>>(tags.size()); 667 668 // For each tag, start resolving the tag name and the tag value. 669 for (final Map.Entry<String, String> entry : tags.entrySet()) { 670 final Deferred<byte[]> name_id = create 671 ? tsdb.tag_names.getOrCreateIdAsync(entry.getKey(), metric, tags) 672 : tsdb.tag_names.getIdAsync(entry.getKey()); 673 final Deferred<byte[]> value_id = create 674 ? tsdb.tag_values.getOrCreateIdAsync(entry.getValue(), metric, tags) 675 : tsdb.tag_values.getIdAsync(entry.getValue()); 676 677 // Then once the tag name is resolved, get the resolved tag value. 678 class TagNameResolvedCB implements Callback<Deferred<byte[]>, byte[]> { 679 public Deferred<byte[]> call(final byte[] nameid) { 680 // And once the tag value too is resolved, paste the two together. 681 class TagValueResolvedCB implements Callback<byte[], byte[]> { 682 public byte[] call(final byte[] valueid) { 683 final byte[] thistag = new byte[nameid.length + valueid.length]; 684 System.arraycopy(nameid, 0, thistag, 0, nameid.length); 685 System.arraycopy(valueid, 0, thistag, nameid.length, valueid.length); 686 return thistag; 687 } 688 } 689 690 return value_id.addCallback(new TagValueResolvedCB()); 691 } 692 } 693 694 // Put all the deferred tag resolutions in this list. 695 final Deferred<byte[]> resolve = 696 name_id.addCallbackDeferring(new TagNameResolvedCB()); 697 tag_ids.add(resolve); 698 } 699 700 // And then once we have all the tags resolved, sort them. 701 return Deferred.group(tag_ids).addCallback(SORT_CB); 702 } 703 704 /** 705 * Sorts a list of tags. 706 * Each entry in the list expected to be a byte array that contains the tag 707 * name UID followed by the tag value UID. 708 */ 709 private static class SortResolvedTagsCB 710 implements Callback<ArrayList<byte[]>, ArrayList<byte[]>> { call(final ArrayList<byte[]> tags)711 public ArrayList<byte[]> call(final ArrayList<byte[]> tags) { 712 // Now sort the tags. 713 Collections.sort(tags, Bytes.MEMCMP); 714 return tags; 715 } 716 } 717 private static final SortResolvedTagsCB SORT_CB = new SortResolvedTagsCB(); 718 719 /** 720 * Resolves all the tags IDs (name followed by value) into the a map. 721 * This function is the opposite of {@link #resolveAll}. 722 * @param tsdb The TSDB to use for UniqueId lookups. 723 * @param tags The tag IDs to resolve. 724 * @return A map mapping tag names to tag values. 725 * @throws NoSuchUniqueId if one of the elements in the array contained an 726 * invalid ID. 727 * @throws IllegalArgumentException if one of the elements in the array had 728 * the wrong number of bytes. 729 */ resolveIds(final TSDB tsdb, final ArrayList<byte[]> tags)730 public static HashMap<String, String> resolveIds(final TSDB tsdb, 731 final ArrayList<byte[]> tags) 732 throws NoSuchUniqueId { 733 try { 734 return resolveIdsAsync(tsdb, tags).joinUninterruptibly(); 735 } catch (NoSuchUniqueId e) { 736 throw e; 737 } catch (Exception e) { 738 throw new RuntimeException("Shouldn't be here", e); 739 } 740 } 741 742 /** 743 * Resolves all the tags IDs asynchronously (name followed by value) into a map. 744 * This function is the opposite of {@link #resolveAll}. 745 * @param tsdb The TSDB to use for UniqueId lookups. 746 * @param tags The tag IDs to resolve. 747 * @return A map mapping tag names to tag values. 748 * @throws NoSuchUniqueId if one of the elements in the array contained an 749 * invalid ID. 750 * @throws IllegalArgumentException if one of the elements in the array had 751 * the wrong number of bytes. 752 * @since 2.0 753 */ resolveIdsAsync(final TSDB tsdb, final List<byte[]> tags)754 public static Deferred<HashMap<String, String>> resolveIdsAsync(final TSDB tsdb, 755 final List<byte[]> tags) 756 throws NoSuchUniqueId { 757 final short name_width = tsdb.tag_names.width(); 758 final short value_width = tsdb.tag_values.width(); 759 final short tag_bytes = (short) (name_width + value_width); 760 final HashMap<String, String> result 761 = new HashMap<String, String>(tags.size()); 762 final ArrayList<Deferred<String>> deferreds 763 = new ArrayList<Deferred<String>>(tags.size()); 764 765 for (final byte[] tag : tags) { 766 final byte[] tmp_name = new byte[name_width]; 767 final byte[] tmp_value = new byte[value_width]; 768 if (tag.length != tag_bytes) { 769 throw new IllegalArgumentException("invalid length: " + tag.length 770 + " (expected " + tag_bytes + "): " + Arrays.toString(tag)); 771 } 772 System.arraycopy(tag, 0, tmp_name, 0, name_width); 773 deferreds.add(tsdb.tag_names.getNameAsync(tmp_name)); 774 System.arraycopy(tag, name_width, tmp_value, 0, value_width); 775 deferreds.add(tsdb.tag_values.getNameAsync(tmp_value)); 776 } 777 778 class GroupCB implements Callback<HashMap<String, String>, ArrayList<String>> { 779 public HashMap<String, String> call(final ArrayList<String> names) 780 throws Exception { 781 for (int i = 0; i < names.size(); i++) { 782 if (i % 2 != 0) { 783 result.put(names.get(i - 1), names.get(i)); 784 } 785 } 786 return result; 787 } 788 } 789 790 return Deferred.groupInOrder(deferreds).addCallback(new GroupCB()); 791 } 792 793 /** 794 * Returns true if the given string looks like an integer. 795 * <p> 796 * This function doesn't do any checking on the string other than looking 797 * for some characters that are generally found in floating point values 798 * such as '.' or 'e'. 799 * @since 1.1 800 */ looksLikeInteger(final String value)801 public static boolean looksLikeInteger(final String value) { 802 final int n = value.length(); 803 for (int i = 0; i < n; i++) { 804 final char c = value.charAt(i); 805 if (c == '.' || c == 'e' || c == 'E') { 806 return false; 807 } 808 } 809 return true; 810 } 811 812 /** 813 * Set the special characters due to allowing for a key or a value of the tag. 814 * @param characters character sequences as a string 815 */ setAllowSpecialChars(String characters)816 public static void setAllowSpecialChars(String characters) { 817 allowSpecialChars = characters == null ? "" : characters; 818 } 819 820 /** 821 * Returns true if the character can be used a tag name or a tag value. 822 * @param character 823 * @return 824 */ isAllowSpecialChars(char character)825 static boolean isAllowSpecialChars(char character) { 826 return allowSpecialChars.indexOf(character) != -1; 827 } 828 } 829