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

org.influxdb.impl.InfluxDBResultMapper Maven / Gradle / Ivy

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2017 azeti Networks AG ()
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.influxdb.impl;

import java.lang.reflect.Field;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import org.influxdb.InfluxDBMapperException;
import org.influxdb.annotation.Column;
import org.influxdb.annotation.Measurement;
import org.influxdb.dto.QueryResult;

/**
 * Main class responsible for mapping a QueryResult to a POJO.
 *
 * @author fmachado
 */
public class InfluxDBResultMapper {

  /**
   * Data structure used to cache classes used as measurements.
   */
  private static final
    ConcurrentMap> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();

  private static final int FRACTION_MIN_WIDTH = 0;
  private static final int FRACTION_MAX_WIDTH = 9;
  private static final boolean ADD_DECIMAL_POINT = true;

  /**
   * When a query is executed without {@link TimeUnit}, InfluxDB returns the time
   * column as a RFC3339 date.
   */
  private static final DateTimeFormatter RFC3339_FORMATTER = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd'T'HH:mm:ss")
    .appendFraction(ChronoField.NANO_OF_SECOND, FRACTION_MIN_WIDTH, FRACTION_MAX_WIDTH, ADD_DECIMAL_POINT)
    .appendZoneOrOffsetId()
    .toFormatter();

  /**
   * 

* Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal * data structure and creating the respective object instances based on the Class passed as * parameter. *

* * @param queryResult the InfluxDB result object * @param clazz the Class that will be used to hold your measurement data * @param the target type * * @return a {@link List} of objects from the same Class passed as parameter and sorted on the * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, * clazz parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz) throws InfluxDBMapperException { return toPOJO(queryResult, clazz, TimeUnit.MILLISECONDS); } /** *

* Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal * data structure and creating the respective object instances based on the Class passed as * parameter. *

* * @param queryResult the InfluxDB result object * @param clazz the Class that will be used to hold your measurement data * @param precision the time precision of results * @param the target type * * @return a {@link List} of objects from the same Class passed as parameter and sorted on the * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, * clazz parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz, final TimeUnit precision) throws InfluxDBMapperException { throwExceptionIfMissingAnnotation(clazz); String measurementName = getMeasurementName(clazz); return this.toPOJO(queryResult, clazz, measurementName, precision); } /** *

* Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal * data structure and creating the respective object instances based on the Class passed as * parameter. *

* * @param queryResult the InfluxDB result object * @param clazz the Class that will be used to hold your measurement data * @param the target type * @param measurementName name of the Measurement * * @return a {@link List} of objects from the same Class passed as parameter and sorted on the * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, * clazz parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz, final String measurementName) throws InfluxDBMapperException { return toPOJO(queryResult, clazz, measurementName, TimeUnit.MILLISECONDS); } /** *

* Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal * data structure and creating the respective object instances based on the Class passed as * parameter. *

* * @param queryResult the InfluxDB result object * @param clazz the Class that will be used to hold your measurement data * @param the target type * @param measurementName name of the Measurement * @param precision the time precision of results * * @return a {@link List} of objects from the same Class passed as parameter and sorted on the * same order as received from InfluxDB. * * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors, * clazz parameter is not annotated with @Measurement or it was not * possible to define the values of your POJO (e.g. due to an unsupported field type). */ public List toPOJO(final QueryResult queryResult, final Class clazz, final String measurementName, final TimeUnit precision) throws InfluxDBMapperException { Objects.requireNonNull(measurementName, "measurementName"); Objects.requireNonNull(queryResult, "queryResult"); Objects.requireNonNull(clazz, "clazz"); throwExceptionIfResultWithError(queryResult); cacheMeasurementClass(clazz); List result = new LinkedList(); queryResult.getResults().stream() .filter(internalResult -> Objects.nonNull(internalResult) && Objects.nonNull(internalResult.getSeries())) .forEach(internalResult -> { internalResult.getSeries().stream() .filter(series -> series.getName().equals(measurementName)) .forEachOrdered(series -> { parseSeriesAs(series, clazz, result, precision); }); }); return result; } void throwExceptionIfMissingAnnotation(final Class clazz) { if (!clazz.isAnnotationPresent(Measurement.class)) { throw new IllegalArgumentException( "Class " + clazz.getName() + " is not annotated with @" + Measurement.class.getSimpleName()); } } void throwExceptionIfResultWithError(final QueryResult queryResult) { if (queryResult.getError() != null) { throw new InfluxDBMapperException("InfluxDB returned an error: " + queryResult.getError()); } queryResult.getResults().forEach(seriesResult -> { if (seriesResult.getError() != null) { throw new InfluxDBMapperException("InfluxDB returned an error with Series: " + seriesResult.getError()); } }); } ConcurrentMap getColNameAndFieldMap(final Class clazz) { return CLASS_FIELD_CACHE.get(clazz.getName()); } void cacheMeasurementClass(final Class... classVarAgrs) { for (Class clazz : classVarAgrs) { if (CLASS_FIELD_CACHE.containsKey(clazz.getName())) { continue; } ConcurrentMap initialMap = new ConcurrentHashMap<>(); ConcurrentMap influxColumnAndFieldMap = CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), initialMap); if (influxColumnAndFieldMap == null) { influxColumnAndFieldMap = initialMap; } Class c = clazz; while (c != null) { for (Field field : c.getDeclaredFields()) { Column colAnnotation = field.getAnnotation(Column.class); if (colAnnotation != null) { influxColumnAndFieldMap.put(colAnnotation.name(), field); } } c = c.getSuperclass(); } } } String getMeasurementName(final Class clazz) { return ((Measurement) clazz.getAnnotation(Measurement.class)).name(); } String getDatabaseName(final Class clazz) { return ((Measurement) clazz.getAnnotation(Measurement.class)).database(); } String getRetentionPolicy(final Class clazz) { return ((Measurement) clazz.getAnnotation(Measurement.class)).retentionPolicy(); } TimeUnit getTimeUnit(final Class clazz) { return ((Measurement) clazz.getAnnotation(Measurement.class)).timeUnit(); } List parseSeriesAs(final QueryResult.Series series, final Class clazz, final List result) { return parseSeriesAs(series, clazz, result, TimeUnit.MILLISECONDS); } List parseSeriesAs(final QueryResult.Series series, final Class clazz, final List result, final TimeUnit precision) { int columnSize = series.getColumns().size(); ConcurrentMap colNameAndFieldMap = CLASS_FIELD_CACHE.get(clazz.getName()); try { T object = null; for (List row : series.getValues()) { for (int i = 0; i < columnSize; i++) { Field correspondingField = colNameAndFieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/); if (correspondingField != null) { if (object == null) { object = clazz.newInstance(); } setFieldValue(object, correspondingField, row.get(i), precision); } } // When the "GROUP BY" clause is used, "tags" are returned as Map and // accordingly with InfluxDB documentation // https://docs.influxdata.com/influxdb/v1.2/concepts/glossary/#tag-value // "tag" values are always String. if (series.getTags() != null && !series.getTags().isEmpty()) { for (Entry entry : series.getTags().entrySet()) { Field correspondingField = colNameAndFieldMap.get(entry.getKey()/*InfluxDB columnName*/); if (correspondingField != null) { // I don't think it is possible to reach here without a valid "object" setFieldValue(object, correspondingField, entry.getValue(), precision); } } } if (object != null) { result.add(object); object = null; } } } catch (InstantiationException | IllegalAccessException e) { throw new InfluxDBMapperException(e); } return result; } /** * InfluxDB client returns any number as Double. * See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987 * for more information. * * @param object * @param field * @param value * @param precision * @throws IllegalArgumentException * @throws IllegalAccessException */ void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision) throws IllegalArgumentException, IllegalAccessException { if (value == null) { return; } Class fieldType = field.getType(); try { if (!field.isAccessible()) { field.setAccessible(true); } if (fieldValueModified(fieldType, field, object, value, precision) || fieldValueForPrimitivesModified(fieldType, field, object, value) || fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) { return; } String msg = "Class '%s' field '%s' is from an unsupported type '%s'."; throw new InfluxDBMapperException( String.format(msg, object.getClass().getName(), field.getName(), field.getType())); } catch (ClassCastException e) { String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. " + "The correct type is '%s' (current field value: '%s')."; throw new InfluxDBMapperException( String.format(msg, object.getClass().getName(), field.getName(), value.getClass().getName(), value)); } } boolean fieldValueModified(final Class fieldType, final Field field, final T object, final Object value, final TimeUnit precision) throws IllegalArgumentException, IllegalAccessException { if (String.class.isAssignableFrom(fieldType)) { field.set(object, String.valueOf(value)); return true; } if (Instant.class.isAssignableFrom(fieldType)) { Instant instant; if (value instanceof String) { instant = Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value))); } else if (value instanceof Long) { instant = Instant.ofEpochMilli(toMillis((long) value, precision)); } else if (value instanceof Double) { instant = Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision)); } else if (value instanceof Integer) { instant = Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision)); } else { throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName()); } field.set(object, instant); return true; } return false; } boolean fieldValueForPrimitivesModified(final Class fieldType, final Field field, final T object, final Object value) throws IllegalArgumentException, IllegalAccessException { if (double.class.isAssignableFrom(fieldType)) { field.setDouble(object, ((Double) value).doubleValue()); return true; } if (long.class.isAssignableFrom(fieldType)) { field.setLong(object, ((Double) value).longValue()); return true; } if (int.class.isAssignableFrom(fieldType)) { field.setInt(object, ((Double) value).intValue()); return true; } if (boolean.class.isAssignableFrom(fieldType)) { field.setBoolean(object, Boolean.valueOf(String.valueOf(value)).booleanValue()); return true; } return false; } boolean fieldValueForPrimitiveWrappersModified(final Class fieldType, final Field field, final T object, final Object value) throws IllegalArgumentException, IllegalAccessException { if (Double.class.isAssignableFrom(fieldType)) { field.set(object, value); return true; } if (Long.class.isAssignableFrom(fieldType)) { field.set(object, Long.valueOf(((Double) value).longValue())); return true; } if (Integer.class.isAssignableFrom(fieldType)) { field.set(object, Integer.valueOf(((Double) value).intValue())); return true; } if (Boolean.class.isAssignableFrom(fieldType)) { field.set(object, Boolean.valueOf(String.valueOf(value))); return true; } return false; } private Long toMillis(final long value, final TimeUnit precision) { return TimeUnit.MILLISECONDS.convert(value, precision); } }