1 package org.influxdb.dto;
2
3 import org.influxdb.BuilderException;
4 import org.influxdb.InfluxDBMapperException;
5 import org.influxdb.annotation.Column;
6 import org.influxdb.annotation.Exclude;
7 import org.influxdb.annotation.Measurement;
8 import org.influxdb.annotation.TimeColumn;
9 import org.influxdb.impl.Preconditions;
10 import org.influxdb.impl.TypeMapper;
11
12 import java.lang.annotation.Annotation;
13 import java.lang.reflect.Field;
14 import java.lang.reflect.Modifier;
15 import java.lang.reflect.ParameterizedType;
16 import java.lang.reflect.Type;
17 import java.math.BigDecimal;
18 import java.math.BigInteger;
19 import java.math.RoundingMode;
20 import java.text.NumberFormat;
21 import java.time.Instant;
22 import java.util.Locale;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.Objects;
26 import java.util.Optional;
27 import java.util.TreeMap;
28 import java.util.concurrent.TimeUnit;
29
30 /**
31  * Representation of a InfluxDB database Point.
32  *
33  * @author stefan.majer [at] gmail.com
34  *
35  */

36 public class Point {
37   private String measurement;
38   private Map<String, String> tags;
39   private Number time;
40   private TimeUnit precision = TimeUnit.NANOSECONDS;
41   private Map<String, Object> fields;
42   private static final int MAX_FRACTION_DIGITS = 340;
43   private static final ThreadLocal<NumberFormat> NUMBER_FORMATTER =
44           ThreadLocal.withInitial(() -> {
45             NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH);
46             numberFormat.setMaximumFractionDigits(MAX_FRACTION_DIGITS);
47             numberFormat.setGroupingUsed(false);
48             numberFormat.setMinimumFractionDigits(1);
49             return numberFormat;
50           });
51
52   private static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
53   private static final ThreadLocal<StringBuilder> CACHED_STRINGBUILDERS =
54           ThreadLocal.withInitial(() -> new StringBuilder(DEFAULT_STRING_BUILDER_SIZE));
55
56   Point() {
57   }
58
59   /**
60    * Create a new Point Build build to create a new Point in a fluent manner.
61    *
62    * @param measurement
63    *            the name of the measurement.
64    * @return the Builder to be able to add further Builder calls.
65    */

66
67   public static Builder measurement(final String measurement) {
68     return new Builder(measurement);
69   }
70
71   /**
72    * Create a new Point Build build to create a new Point in a fluent manner from a POJO.
73    *
74    * @param clazz Class of the POJO
75    * @return the Builder instance
76    */

77
78   public static Builder measurementByPOJO(final Class<?> clazz) {
79     Objects.requireNonNull(clazz, "clazz");
80     throwExceptionIfMissingAnnotation(clazz, Measurement.class);
81     String measurementName = findMeasurementName(clazz);
82     return new Builder(measurementName);
83   }
84
85   private static void throwExceptionIfMissingAnnotation(final Class<?> clazz,
86       final Class<? extends Annotation> expectedClass) {
87     if (!clazz.isAnnotationPresent(expectedClass)) {
88       throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @"
89           + Measurement.class.getSimpleName());
90     }
91   }
92
93   /**
94    * Builder for a new Point.
95    *
96    * @author stefan.majer [at] gmail.com
97    *
98    */

99   public static final class Builder {
100     private static final BigInteger NANOSECONDS_PER_SECOND = BigInteger.valueOf(1000000000L);
101     private final String measurement;
102     private final Map<String, String> tags = new TreeMap<>();
103     private Number time;
104     private TimeUnit precision;
105     private final Map<String, Object> fields = new TreeMap<>();
106
107     /**
108      * @param measurement
109      */

110     Builder(final String measurement) {
111       this.measurement = measurement;
112     }
113
114     /**
115      * Add a tag to this point.
116      *
117      * @param tagName
118      *            the tag name
119      * @param value
120      *            the tag value
121      * @return the Builder instance.
122      */

123     public Builder tag(final String tagName, final String value) {
124       Objects.requireNonNull(tagName, "tagName");
125       Objects.requireNonNull(value, "value");
126       if (!tagName.isEmpty() && !value.isEmpty()) {
127         tags.put(tagName, value);
128       }
129       return this;
130     }
131
132     /**
133      * Add a Map of tags to add to this point.
134      *
135      * @param tagsToAdd
136      *            the Map of tags to add
137      * @return the Builder instance.
138      */

139     public Builder tag(final Map<String, String> tagsToAdd) {
140       for (Entry<String, String> tag : tagsToAdd.entrySet()) {
141         tag(tag.getKey(), tag.getValue());
142       }
143       return this;
144     }
145
146     /**
147      * Add a field to this point.
148      *
149      * @param field
150      *            the field name
151      * @param value
152      *            the value of this field
153      * @return the Builder instance.
154      */

155     @SuppressWarnings("checkstyle:finalparameters")
156     @Deprecated
157     public Builder field(final String field, Object value) {
158       if (value instanceof Number) {
159         if (value instanceof Byte) {
160           value = ((Byte) value).doubleValue();
161         } else if (value instanceof Short) {
162           value = ((Short) value).doubleValue();
163         } else if (value instanceof Integer) {
164           value = ((Integer) value).doubleValue();
165         } else if (value instanceof Long) {
166           value = ((Long) value).doubleValue();
167         } else if (value instanceof BigInteger) {
168           value = ((BigInteger) value).doubleValue();
169         }
170       }
171       fields.put(field, value);
172       return this;
173     }
174
175     public Builder addField(final String field, final boolean value) {
176       fields.put(field, value);
177       return this;
178     }
179
180     public Builder addField(final String field, final long value) {
181       fields.put(field, value);
182       return this;
183     }
184
185     public Builder addField(final String field, final double value) {
186       fields.put(field, value);
187       return this;
188     }
189
190     public Builder addField(final String field, final int value) {
191       fields.put(field, value);
192       return this;
193     }
194
195     public Builder addField(final String field, final float value) {
196       fields.put(field, value);
197       return this;
198     }
199
200     public Builder addField(final String field, final short value) {
201       fields.put(field, value);
202       return this;
203     }
204
205     public Builder addField(final String field, final Number value) {
206       fields.put(field, value);
207       return this;
208     }
209
210     public Builder addField(final String field, final String value) {
211       Objects.requireNonNull(value, "value");
212
213       fields.put(field, value);
214       return this;
215     }
216
217     /**
218      * Add a Map of fields to this point.
219      *
220      * @param fieldsToAdd
221      *            the fields to add
222      * @return the Builder instance.
223      */

224     public Builder fields(final Map<String, Object> fieldsToAdd) {
225       this.fields.putAll(fieldsToAdd);
226       return this;
227     }
228
229     /**
230      * Add a time to this point.
231      *
232      * @param timeToSet      the time for this point
233      * @param precisionToSet the TimeUnit
234      * @return the Builder instance.
235      */

236     public Builder time(final Number timeToSet, final TimeUnit precisionToSet) {
237       Objects.requireNonNull(timeToSet, "timeToSet");
238       Objects.requireNonNull(precisionToSet, "precisionToSet");
239       this.time = timeToSet;
240       this.precision = precisionToSet;
241       return this;
242     }
243
244     /**
245      * Add a time to this point as long.
246      * only kept for binary compatibility with previous releases.
247      *
248      * @param timeToSet      the time for this point as long
249      * @param precisionToSet the TimeUnit
250      * @return the Builder instance.
251      */

252     public Builder time(final long timeToSet, final TimeUnit precisionToSet) {
253       return time((Number) timeToSet, precisionToSet);
254     }
255
256     /**
257      * Add a time to this point as Long.
258      * only kept for binary compatibility with previous releases.
259      *
260      * @param timeToSet      the time for this point as Long
261      * @param precisionToSet the TimeUnit
262      * @return the Builder instance.
263      */

264     public Builder time(final Long timeToSet, final TimeUnit precisionToSet) {
265       return time((Number) timeToSet, precisionToSet);
266     }
267
268     /**
269      * Does this builder contain any fields?
270      *
271      * @return trueif the builder contains any fields, false otherwise.
272      */

273     public boolean hasFields() {
274       return !fields.isEmpty();
275     }
276
277     /**
278      * Adds field map from object by reflection using {@link org.influxdb.annotation.Column}
279      * annotation.
280      *
281      * @param pojo POJO Object with annotation {@link org.influxdb.annotation.Column} on fields
282      * @return the Builder instance
283      */

284     public Builder addFieldsFromPOJO(final Object pojo) {
285
286       Class<?> clazz = pojo.getClass();
287       Measurement measurement = clazz.getAnnotation(Measurement.class);
288       boolean allFields = measurement != null && measurement.allFields();
289
290       while (clazz != null) {
291
292       TypeMapper typeMapper = TypeMapper.empty();
293       while (clazz != null) {
294         for (Field field : clazz.getDeclaredFields()) {
295
296           Column column = field.getAnnotation(Column.class);
297
298           if (column == null && !(allFields
299                   && !field.isAnnotationPresent(Exclude.class) && !Modifier.isStatic(field.getModifiers()))) {
300             continue;
301           }
302
303           field.setAccessible(true);
304
305           String fieldName;
306           if (column != null && !column.name().isEmpty()) {
307             fieldName = column.name();
308           } else {
309             fieldName = field.getName();
310           }
311
312           addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName, typeMapper);
313         }
314
315         Class<?> superclass = clazz.getSuperclass();
316         Type genericSuperclass = clazz.getGenericSuperclass();
317         if (genericSuperclass instanceof ParameterizedType) {
318           typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass);
319         } else {
320           typeMapper = TypeMapper.empty();
321         }
322
323         clazz = superclass;
324       }
325     }
326
327       if (this.fields.isEmpty()) {
328         throw new BuilderException("Class " + pojo.getClass().getName()
329             + " has no @" + Column.class.getSimpleName() + " annotation");
330       }
331
332       return this;
333     }
334
335     private void addFieldByAttribute(final Object pojo, final Field field, final boolean tag,
336                                      final String fieldName, final TypeMapper typeMapper) {
337       try {
338         Object fieldValue = field.get(pojo);
339
340         TimeColumn tc = field.getAnnotation(TimeColumn.class);
341         Class<?> fieldType = (Class<?>) typeMapper.resolve(field.getGenericType());
342         if (tc != null) {
343           if (Instant.class.isAssignableFrom(fieldType)) {
344             Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> {
345               TimeUnit timeUnit = tc.timeUnit();
346               if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) {
347                 this.time = BigInteger.valueOf(instant.getEpochSecond())
348                         .multiply(NANOSECONDS_PER_SECOND)
349                         .add(BigInteger.valueOf(instant.getNano()))
350                         .divide(BigInteger.valueOf(TimeUnit.NANOSECONDS.convert(1, timeUnit)));
351               } else {
352                 this.time = timeUnit.convert(instant.toEpochMilli(), TimeUnit.MILLISECONDS);
353               }
354               this.precision = timeUnit;
355             });
356             return;
357           }
358
359           throw new InfluxDBMapperException(
360               "Unsupported type " + fieldType + for time: should be of Instant type");
361         }
362
363         if (tag) {
364           if (fieldValue != null) {
365             this.tags.put(fieldName, (String) fieldValue);
366           }
367         } else {
368           if (fieldValue != null) {
369             setField(fieldType, fieldName, fieldValue);
370           }
371         }
372
373       } catch (IllegalArgumentException | IllegalAccessException e) {
374         // Can not happen since we use metadata got from the object
375         throw new BuilderException(
376             "Field " + fieldName + " could not found on class " + pojo.getClass().getSimpleName());
377       }
378     }
379
380     /**
381      * Create a new Point.
382      *
383      * @return the newly created Point.
384      */

385     public Point build() {
386       Preconditions.checkNonEmptyString(this.measurement, "measurement");
387       Preconditions.checkPositiveNumber(this.fields.size(), "fields size");
388       Point point = new Point();
389       point.setFields(this.fields);
390       point.setMeasurement(this.measurement);
391       if (this.time != null) {
392           point.setTime(this.time);
393           point.setPrecision(this.precision);
394       }
395       point.setTags(this.tags);
396       return point;
397     }
398
399     private void setField(
400             final Class<?> fieldType,
401             final String columnName,
402             final Object value) {
403       if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
404         addField(columnName, (boolean) value);
405       } else if (long.class.isAssignableFrom(fieldType) || Long.class.isAssignableFrom(fieldType)) {
406         addField(columnName, (long) value);
407       } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
408         addField(columnName, (double) value);
409       } else if (float.class.isAssignableFrom(fieldType) || Float.class.isAssignableFrom(fieldType)) {
410         addField(columnName, (float) value);
411       } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
412         addField(columnName, (int) value);
413       } else if (short.class.isAssignableFrom(fieldType) || Short.class.isAssignableFrom(fieldType)) {
414         addField(columnName, (short) value);
415       } else if (String.class.isAssignableFrom(fieldType)) {
416         addField(columnName, (String) value);
417       } else if (Enum.class.isAssignableFrom(fieldType)) {
418         addField(columnName, ((Enum<?>) value).name());
419       } else {
420         throw new InfluxDBMapperException(
421                 "Unsupported type " + fieldType + for column " + columnName);
422       }
423     }
424   }
425
426   /**
427    * @param measurement
428    *            the measurement to set
429    */

430   void setMeasurement(final String measurement) {
431     this.measurement = measurement;
432   }
433
434   /**
435    * @param time
436    *            the time to set
437    */

438   void setTime(final Number time) {
439     this.time = time;
440   }
441
442   /**
443    * @param tags
444    *            the tags to set
445    */

446   void setTags(final Map<String, String> tags) {
447     this.tags = tags;
448   }
449
450   /**
451    * @return the tags
452    */

453   Map<String, String> getTags() {
454     return this.tags;
455   }
456
457   /**
458    * @param precision
459    *            the precision to set
460    */

461   void setPrecision(final TimeUnit precision) {
462     this.precision = precision;
463   }
464
465   /**
466    * @return the fields
467    */

468   Map<String, Object> getFields() {
469     return this.fields;
470   }
471
472   /**
473    * @param fields
474    *            the fields to set
475    */

476   void setFields(final Map<String, Object> fields) {
477     this.fields = fields;
478   }
479
480   @Override
481   public boolean equals(final Object o) {
482     if (this == o) {
483       return true;
484     }
485     if (o == null || getClass() != o.getClass()) {
486       return false;
487     }
488     Point point = (Point) o;
489     return Objects.equals(measurement, point.measurement)
490             && Objects.equals(tags, point.tags)
491             && Objects.equals(time, point.time)
492             && precision == point.precision
493             && Objects.equals(fields, point.fields);
494   }
495
496   @Override
497   public int hashCode() {
498     return Objects.hash(measurement, tags, time, precision, fields);
499   }
500
501   /**
502    * {@inheritDoc}
503    */

504   @Override
505   public String toString() {
506     StringBuilder builder = new StringBuilder();
507     builder.append("Point [name=");
508     builder.append(this.measurement);
509     if (this.time != null) {
510       builder.append(", time=");
511       builder.append(this.time);
512     }
513     builder.append(", tags=");
514     builder.append(this.tags);
515     if (this.precision != null) {
516       builder.append(", precision=");
517       builder.append(this.precision);
518     }
519     builder.append(", fields=");
520     builder.append(this.fields);
521     builder.append("]");
522     return builder.toString();
523   }
524
525   /**
526    * Calculate the lineprotocol entry for a single Point.
527    * <p>
528    * NaN and infinity values are silently dropped as they are unsupported:
529    * https://github.com/influxdata/influxdb/issues/4089
530    *
531    * @see <a href="https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_reference/">
532    *     InfluxDB line protocol reference</a>
533    *
534    * @return the String without newLine, empty when there are no fields to write
535    */

536   public String lineProtocol() {
537     return lineProtocol(null);
538   }
539
540   /**
541    * Calculate the lineprotocol entry for a single point, using a specific {@link TimeUnit} for the timestamp.
542    * <p>
543    * NaN and infinity values are silently dropped as they are unsupported:
544    * https://github.com/influxdata/influxdb/issues/4089
545    *
546    * @see <a href="https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_reference/">
547    *     InfluxDB line protocol reference</a>
548    *
549    * @param precision the time precision unit for this point
550    * @return the String without newLine, empty when there are no fields to write
551    */

552   public String lineProtocol(final TimeUnit precision) {
553
554     // setLength(0) is used for reusing cached StringBuilder instance per thread
555     // it reduces GC activity and performs better then new StringBuilder()
556     StringBuilder sb = CACHED_STRINGBUILDERS.get();
557     sb.setLength(0);
558
559     escapeKey(sb, measurement);
560     concatenatedTags(sb);
561     int writtenFields = concatenatedFields(sb);
562     if (writtenFields == 0) {
563       return "";
564     }
565     formatedTime(sb, precision);
566
567     return sb.toString();
568   }
569
570   private void concatenatedTags(final StringBuilder sb) {
571     for (Entry<String, String> tag : this.tags.entrySet()) {
572       sb.append(',');
573       escapeKey(sb, tag.getKey());
574       sb.append('=');
575       escapeKey(sb, tag.getValue());
576     }
577     sb.append(' ');
578   }
579
580   private int concatenatedFields(final StringBuilder sb) {
581     int fieldCount = 0;
582     for (Entry<String, Object> field : this.fields.entrySet()) {
583       Object value = field.getValue();
584       if (value == null || isNotFinite(value)) {
585         continue;
586       }
587       escapeKey(sb, field.getKey());
588       sb.append('=');
589       if (value instanceof Number) {
590         if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
591           sb.append(NUMBER_FORMATTER.get().format(value));
592         } else {
593           sb.append(value).append('i');
594         }
595       } else if (value instanceof String) {
596         String stringValue = (String) value;
597         sb.append('"');
598         escapeField(sb, stringValue);
599         sb.append('"');
600       } else {
601         sb.append(value);
602       }
603
604       sb.append(',');
605
606       fieldCount++;
607     }
608
609     // efficiently chop off the trailing comma
610     int lengthMinusOne = sb.length() - 1;
611     if (sb.charAt(lengthMinusOne) == ',') {
612       sb.setLength(lengthMinusOne);
613     }
614
615     return fieldCount;
616   }
617
618   static void escapeKey(final StringBuilder sb, final String key) {
619     for (int i = 0; i < key.length(); i++) {
620       switch (key.charAt(i)) {
621         case ' ':
622         case ',':
623         case '=':
624           sb.append('\\');
625         default:
626           sb.append(key.charAt(i));
627       }
628     }
629   }
630
631   static void escapeField(final StringBuilder sb, final String field) {
632     for (int i = 0; i < field.length(); i++) {
633       switch (field.charAt(i)) {
634         case '\\':
635         case '\"':
636           sb.append('\\');
637         default:
638           sb.append(field.charAt(i));
639       }
640     }
641   }
642
643   private static boolean isNotFinite(final Object value) {
644     return value instanceof Double && !Double.isFinite((Double) value)
645             || value instanceof Float && !Float.isFinite((Float) value);
646   }
647
648   private void formatedTime(final StringBuilder sb, final TimeUnit precision) {
649     if (this.time == null) {
650       return;
651     }
652     TimeUnit converterPrecision = precision;
653
654     if (converterPrecision == null) {
655       converterPrecision = TimeUnit.NANOSECONDS;
656     }
657     if (this.time instanceof BigInteger) {
658       BigInteger time = (BigInteger) this.time;
659       long conversionFactor = converterPrecision.convert(1, this.precision);
660       if (conversionFactor >= 1) {
661         time = time.multiply(BigInteger.valueOf(conversionFactor));
662       } else {
663         conversionFactor = this.precision.convert(1, converterPrecision);
664         time = time.divide(BigInteger.valueOf(conversionFactor));
665       }
666       sb.append(" ").append(time);
667     } else if (this.time instanceof BigDecimal) {
668       BigDecimal time = (BigDecimal) this.time;
669       long conversionFactor = converterPrecision.convert(1, this.precision);
670       if (conversionFactor >= 1) {
671         time = time.multiply(BigDecimal.valueOf(conversionFactor));
672       } else {
673         conversionFactor = this.precision.convert(1, converterPrecision);
674         time = time.divide(BigDecimal.valueOf(conversionFactor), RoundingMode.HALF_UP);
675       }
676       sb.append(" ").append(time.toBigInteger());
677     } else {
678       sb.append(" ").append(converterPrecision.convert(this.time.longValue(), this.precision));
679     }
680   }
681
682
683   private static String findMeasurementName(final Class<?> clazz) {
684     return clazz.getAnnotation(Measurement.class).name();
685   }
686 }
687