All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.influxdb.dto.Point Maven / Gradle / Ivy

The newest version!
package org.influxdb.dto;

import org.influxdb.BuilderException;
import org.influxdb.InfluxDBMapperException;
import org.influxdb.annotation.Column;
import org.influxdb.annotation.Exclude;
import org.influxdb.annotation.Measurement;
import org.influxdb.annotation.TimeColumn;
import org.influxdb.impl.Preconditions;
import org.influxdb.impl.TypeMapper;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.time.Instant;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
 * Representation of a InfluxDB database Point.
 *
 * @author stefan.majer [at] gmail.com
 *
 */
public class Point {
  private String measurement;
  private Map tags;
  private Number time;
  private TimeUnit precision = TimeUnit.NANOSECONDS;
  private Map fields;
  private static final int MAX_FRACTION_DIGITS = 340;
  private static final ThreadLocal NUMBER_FORMATTER =
          ThreadLocal.withInitial(() -> {
            NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH);
            numberFormat.setMaximumFractionDigits(MAX_FRACTION_DIGITS);
            numberFormat.setGroupingUsed(false);
            numberFormat.setMinimumFractionDigits(1);
            return numberFormat;
          });

  private static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
  private static final ThreadLocal CACHED_STRINGBUILDERS =
          ThreadLocal.withInitial(() -> new StringBuilder(DEFAULT_STRING_BUILDER_SIZE));

  Point() {
  }

  /**
   * Create a new Point Build build to create a new Point in a fluent manner.
   *
   * @param measurement
   *            the name of the measurement.
   * @return the Builder to be able to add further Builder calls.
   */

  public static Builder measurement(final String measurement) {
    return new Builder(measurement);
  }

  /**
   * Create a new Point Build build to create a new Point in a fluent manner from a POJO.
   *
   * @param clazz Class of the POJO
   * @return the Builder instance
   */

  public static Builder measurementByPOJO(final Class clazz) {
    Objects.requireNonNull(clazz, "clazz");
    throwExceptionIfMissingAnnotation(clazz, Measurement.class);
    String measurementName = findMeasurementName(clazz);
    return new Builder(measurementName);
  }

  private static void throwExceptionIfMissingAnnotation(final Class clazz,
      final Class expectedClass) {
    if (!clazz.isAnnotationPresent(expectedClass)) {
      throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @"
          + Measurement.class.getSimpleName());
    }
  }

  /**
   * Builder for a new Point.
   *
   * @author stefan.majer [at] gmail.com
   *
   */
  public static final class Builder {
    private static final BigInteger NANOSECONDS_PER_SECOND = BigInteger.valueOf(1000000000L);
    private final String measurement;
    private final Map tags = new TreeMap<>();
    private Number time;
    private TimeUnit precision;
    private final Map fields = new TreeMap<>();

    /**
     * @param measurement
     */
    Builder(final String measurement) {
      this.measurement = measurement;
    }

    /**
     * Add a tag to this point.
     *
     * @param tagName
     *            the tag name
     * @param value
     *            the tag value
     * @return the Builder instance.
     */
    public Builder tag(final String tagName, final String value) {
      Objects.requireNonNull(tagName, "tagName");
      Objects.requireNonNull(value, "value");
      if (!tagName.isEmpty() && !value.isEmpty()) {
        tags.put(tagName, value);
      }
      return this;
    }

    /**
     * Add a Map of tags to add to this point.
     *
     * @param tagsToAdd
     *            the Map of tags to add
     * @return the Builder instance.
     */
    public Builder tag(final Map tagsToAdd) {
      for (Entry tag : tagsToAdd.entrySet()) {
        tag(tag.getKey(), tag.getValue());
      }
      return this;
    }

    /**
     * Add a field to this point.
     *
     * @param field
     *            the field name
     * @param value
     *            the value of this field
     * @return the Builder instance.
     */
    @SuppressWarnings("checkstyle:finalparameters")
    @Deprecated
    public Builder field(final String field, Object value) {
      if (value instanceof Number) {
        if (value instanceof Byte) {
          value = ((Byte) value).doubleValue();
        } else if (value instanceof Short) {
          value = ((Short) value).doubleValue();
        } else if (value instanceof Integer) {
          value = ((Integer) value).doubleValue();
        } else if (value instanceof Long) {
          value = ((Long) value).doubleValue();
        } else if (value instanceof BigInteger) {
          value = ((BigInteger) value).doubleValue();
        }
      }
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final boolean value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final long value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final double value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final int value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final float value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final short value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final Number value) {
      fields.put(field, value);
      return this;
    }

    public Builder addField(final String field, final String value) {
      Objects.requireNonNull(value, "value");

      fields.put(field, value);
      return this;
    }

    /**
     * Add a Map of fields to this point.
     *
     * @param fieldsToAdd
     *            the fields to add
     * @return the Builder instance.
     */
    public Builder fields(final Map fieldsToAdd) {
      this.fields.putAll(fieldsToAdd);
      return this;
    }

    /**
     * Add a time to this point.
     *
     * @param timeToSet      the time for this point
     * @param precisionToSet the TimeUnit
     * @return the Builder instance.
     */
    public Builder time(final Number timeToSet, final TimeUnit precisionToSet) {
      Objects.requireNonNull(timeToSet, "timeToSet");
      Objects.requireNonNull(precisionToSet, "precisionToSet");
      this.time = timeToSet;
      this.precision = precisionToSet;
      return this;
    }

    /**
     * Add a time to this point as long.
     * only kept for binary compatibility with previous releases.
     *
     * @param timeToSet      the time for this point as long
     * @param precisionToSet the TimeUnit
     * @return the Builder instance.
     */
    public Builder time(final long timeToSet, final TimeUnit precisionToSet) {
      return time((Number) timeToSet, precisionToSet);
    }

    /**
     * Add a time to this point as Long.
     * only kept for binary compatibility with previous releases.
     *
     * @param timeToSet      the time for this point as Long
     * @param precisionToSet the TimeUnit
     * @return the Builder instance.
     */
    public Builder time(final Long timeToSet, final TimeUnit precisionToSet) {
      return time((Number) timeToSet, precisionToSet);
    }

    /**
     * Does this builder contain any fields?
     *
     * @return true, if the builder contains any fields, false otherwise.
     */
    public boolean hasFields() {
      return !fields.isEmpty();
    }

    /**
     * Adds field map from object by reflection using {@link org.influxdb.annotation.Column}
     * annotation.
     *
     * @param pojo POJO Object with annotation {@link org.influxdb.annotation.Column} on fields
     * @return the Builder instance
     */
    public Builder addFieldsFromPOJO(final Object pojo) {

      Class clazz = pojo.getClass();
      Measurement measurement = clazz.getAnnotation(Measurement.class);
      boolean allFields = measurement != null && measurement.allFields();

      while (clazz != null) {

      TypeMapper typeMapper = TypeMapper.empty();
      while (clazz != null) {
        for (Field field : clazz.getDeclaredFields()) {

          Column column = field.getAnnotation(Column.class);

          if (column == null && !(allFields
                  && !field.isAnnotationPresent(Exclude.class) && !Modifier.isStatic(field.getModifiers()))) {
            continue;
          }

          field.setAccessible(true);

          String fieldName;
          if (column != null && !column.name().isEmpty()) {
            fieldName = column.name();
          } else {
            fieldName = field.getName();
          }

          addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName, typeMapper);
        }

        Class superclass = clazz.getSuperclass();
        Type genericSuperclass = clazz.getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
          typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass);
        } else {
          typeMapper = TypeMapper.empty();
        }

        clazz = superclass;
      }
    }

      if (this.fields.isEmpty()) {
        throw new BuilderException("Class " + pojo.getClass().getName()
            + " has no @" + Column.class.getSimpleName() + " annotation");
      }

      return this;
    }

    private void addFieldByAttribute(final Object pojo, final Field field, final boolean tag,
                                     final String fieldName, final TypeMapper typeMapper) {
      try {
        Object fieldValue = field.get(pojo);

        TimeColumn tc = field.getAnnotation(TimeColumn.class);
        Class fieldType = (Class) typeMapper.resolve(field.getGenericType());
        if (tc != null) {
          if (Instant.class.isAssignableFrom(fieldType)) {
            Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> {
              TimeUnit timeUnit = tc.timeUnit();
              if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) {
                this.time = BigInteger.valueOf(instant.getEpochSecond())
                        .multiply(NANOSECONDS_PER_SECOND)
                        .add(BigInteger.valueOf(instant.getNano()))
                        .divide(BigInteger.valueOf(TimeUnit.NANOSECONDS.convert(1, timeUnit)));
              } else {
                this.time = timeUnit.convert(instant.toEpochMilli(), TimeUnit.MILLISECONDS);
              }
              this.precision = timeUnit;
            });
            return;
          }

          throw new InfluxDBMapperException(
              "Unsupported type " + fieldType + " for time: should be of Instant type");
        }

        if (tag) {
          if (fieldValue != null) {
            this.tags.put(fieldName, (String) fieldValue);
          }
        } else {
          if (fieldValue != null) {
            setField(fieldType, fieldName, fieldValue);
          }
        }

      } catch (IllegalArgumentException | IllegalAccessException e) {
        // Can not happen since we use metadata got from the object
        throw new BuilderException(
            "Field " + fieldName + " could not found on class " + pojo.getClass().getSimpleName());
      }
    }

    /**
     * Create a new Point.
     *
     * @return the newly created Point.
     */
    public Point build() {
      Preconditions.checkNonEmptyString(this.measurement, "measurement");
      Preconditions.checkPositiveNumber(this.fields.size(), "fields size");
      Point point = new Point();
      point.setFields(this.fields);
      point.setMeasurement(this.measurement);
      if (this.time != null) {
          point.setTime(this.time);
          point.setPrecision(this.precision);
      }
      point.setTags(this.tags);
      return point;
    }

    private void setField(
            final Class fieldType,
            final String columnName,
            final Object value) {
      if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) {
        addField(columnName, (boolean) value);
      } else if (long.class.isAssignableFrom(fieldType) || Long.class.isAssignableFrom(fieldType)) {
        addField(columnName, (long) value);
      } else if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
        addField(columnName, (double) value);
      } else if (float.class.isAssignableFrom(fieldType) || Float.class.isAssignableFrom(fieldType)) {
        addField(columnName, (float) value);
      } else if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
        addField(columnName, (int) value);
      } else if (short.class.isAssignableFrom(fieldType) || Short.class.isAssignableFrom(fieldType)) {
        addField(columnName, (short) value);
      } else if (String.class.isAssignableFrom(fieldType)) {
        addField(columnName, (String) value);
      } else if (Enum.class.isAssignableFrom(fieldType)) {
        addField(columnName, ((Enum) value).name());
      } else {
        throw new InfluxDBMapperException(
                "Unsupported type " + fieldType + " for column " + columnName);
      }
    }
  }

  /**
   * @param measurement
   *            the measurement to set
   */
  void setMeasurement(final String measurement) {
    this.measurement = measurement;
  }

  /**
   * @param time
   *            the time to set
   */
  void setTime(final Number time) {
    this.time = time;
  }

  /**
   * @param tags
   *            the tags to set
   */
  void setTags(final Map tags) {
    this.tags = tags;
  }

  /**
   * @return the tags
   */
  Map getTags() {
    return this.tags;
  }

  /**
   * @param precision
   *            the precision to set
   */
  void setPrecision(final TimeUnit precision) {
    this.precision = precision;
  }

  /**
   * @return the fields
   */
  Map getFields() {
    return this.fields;
  }

  /**
   * @param fields
   *            the fields to set
   */
  void setFields(final Map fields) {
    this.fields = fields;
  }

  @Override
  public boolean equals(final Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    Point point = (Point) o;
    return Objects.equals(measurement, point.measurement)
            && Objects.equals(tags, point.tags)
            && Objects.equals(time, point.time)
            && precision == point.precision
            && Objects.equals(fields, point.fields);
  }

  @Override
  public int hashCode() {
    return Objects.hash(measurement, tags, time, precision, fields);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("Point [name=");
    builder.append(this.measurement);
    if (this.time != null) {
      builder.append(", time=");
      builder.append(this.time);
    }
    builder.append(", tags=");
    builder.append(this.tags);
    if (this.precision != null) {
      builder.append(", precision=");
      builder.append(this.precision);
    }
    builder.append(", fields=");
    builder.append(this.fields);
    builder.append("]");
    return builder.toString();
  }

  /**
   * Calculate the lineprotocol entry for a single Point.
   * 

* NaN and infinity values are silently dropped as they are unsupported: * https://github.com/influxdata/influxdb/issues/4089 * * @see * InfluxDB line protocol reference * * @return the String without newLine, empty when there are no fields to write */ public String lineProtocol() { return lineProtocol(null); } /** * Calculate the lineprotocol entry for a single point, using a specific {@link TimeUnit} for the timestamp. *

* NaN and infinity values are silently dropped as they are unsupported: * https://github.com/influxdata/influxdb/issues/4089 * * @see * InfluxDB line protocol reference * * @param precision the time precision unit for this point * @return the String without newLine, empty when there are no fields to write */ public String lineProtocol(final TimeUnit precision) { // setLength(0) is used for reusing cached StringBuilder instance per thread // it reduces GC activity and performs better then new StringBuilder() StringBuilder sb = CACHED_STRINGBUILDERS.get(); sb.setLength(0); escapeKey(sb, measurement); concatenatedTags(sb); int writtenFields = concatenatedFields(sb); if (writtenFields == 0) { return ""; } formatedTime(sb, precision); return sb.toString(); } private void concatenatedTags(final StringBuilder sb) { for (Entry tag : this.tags.entrySet()) { sb.append(','); escapeKey(sb, tag.getKey()); sb.append('='); escapeKey(sb, tag.getValue()); } sb.append(' '); } private int concatenatedFields(final StringBuilder sb) { int fieldCount = 0; for (Entry field : this.fields.entrySet()) { Object value = field.getValue(); if (value == null || isNotFinite(value)) { continue; } escapeKey(sb, field.getKey()); sb.append('='); if (value instanceof Number) { if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) { sb.append(NUMBER_FORMATTER.get().format(value)); } else { sb.append(value).append('i'); } } else if (value instanceof String) { String stringValue = (String) value; sb.append('"'); escapeField(sb, stringValue); sb.append('"'); } else { sb.append(value); } sb.append(','); fieldCount++; } // efficiently chop off the trailing comma int lengthMinusOne = sb.length() - 1; if (sb.charAt(lengthMinusOne) == ',') { sb.setLength(lengthMinusOne); } return fieldCount; } static void escapeKey(final StringBuilder sb, final String key) { for (int i = 0; i < key.length(); i++) { switch (key.charAt(i)) { case ' ': case ',': case '=': sb.append('\\'); default: sb.append(key.charAt(i)); } } } static void escapeField(final StringBuilder sb, final String field) { for (int i = 0; i < field.length(); i++) { switch (field.charAt(i)) { case '\\': case '\"': sb.append('\\'); default: sb.append(field.charAt(i)); } } } private static boolean isNotFinite(final Object value) { return value instanceof Double && !Double.isFinite((Double) value) || value instanceof Float && !Float.isFinite((Float) value); } private void formatedTime(final StringBuilder sb, final TimeUnit precision) { if (this.time == null) { return; } TimeUnit converterPrecision = precision; if (converterPrecision == null) { converterPrecision = TimeUnit.NANOSECONDS; } if (this.time instanceof BigInteger) { BigInteger time = (BigInteger) this.time; long conversionFactor = converterPrecision.convert(1, this.precision); if (conversionFactor >= 1) { time = time.multiply(BigInteger.valueOf(conversionFactor)); } else { conversionFactor = this.precision.convert(1, converterPrecision); time = time.divide(BigInteger.valueOf(conversionFactor)); } sb.append(" ").append(time); } else if (this.time instanceof BigDecimal) { BigDecimal time = (BigDecimal) this.time; long conversionFactor = converterPrecision.convert(1, this.precision); if (conversionFactor >= 1) { time = time.multiply(BigDecimal.valueOf(conversionFactor)); } else { conversionFactor = this.precision.convert(1, converterPrecision); time = time.divide(BigDecimal.valueOf(conversionFactor), RoundingMode.HALF_UP); } sb.append(" ").append(time.toBigInteger()); } else { sb.append(" ").append(converterPrecision.convert(this.time.longValue(), this.precision)); } } private static String findMeasurementName(final Class clazz) { return clazz.getAnnotation(Measurement.class).name(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy