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