org.influxdb.dto.Point Maven / Gradle / Ivy
Show all versions of influxdb-java Show documentation
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 extends Annotation> 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();
}
}