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
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
66
67 public static Builder measurement(final String measurement) {
68 return new Builder(measurement);
69 }
70
71
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
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
110 Builder(final String measurement) {
111 this.measurement = measurement;
112 }
113
114
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
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
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
224 public Builder fields(final Map<String, Object> fieldsToAdd) {
225 this.fields.putAll(fieldsToAdd);
226 return this;
227 }
228
229
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
252 public Builder time(final long timeToSet, final TimeUnit precisionToSet) {
253 return time((Number) timeToSet, precisionToSet);
254 }
255
256
264 public Builder time(final Long timeToSet, final TimeUnit precisionToSet) {
265 return time((Number) timeToSet, precisionToSet);
266 }
267
268
273 public boolean hasFields() {
274 return !fields.isEmpty();
275 }
276
277
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
375 throw new BuilderException(
376 "Field " + fieldName + " could not found on class " + pojo.getClass().getSimpleName());
377 }
378 }
379
380
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
430 void setMeasurement(final String measurement) {
431 this.measurement = measurement;
432 }
433
434
438 void setTime(final Number time) {
439 this.time = time;
440 }
441
442
446 void setTags(final Map<String, String> tags) {
447 this.tags = tags;
448 }
449
450
453 Map<String, String> getTags() {
454 return this.tags;
455 }
456
457
461 void setPrecision(final TimeUnit precision) {
462 this.precision = precision;
463 }
464
465
468 Map<String, Object> getFields() {
469 return this.fields;
470 }
471
472
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
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
536 public String lineProtocol() {
537 return lineProtocol(null);
538 }
539
540
552 public String lineProtocol(final TimeUnit precision) {
553
554
555
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
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