@@ -404,5 +408,50 @@
+
+
+ java17
+
+ 17
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-test-source
+ generate-test-sources
+
+ add-test-source
+
+
+
+ src/test-jdk17/java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ true
+
+
+ 17
+ 17
+
+
+ -parameters
+ --add-opens=java.base/java.lang=ALL-UNNAMED
+ --add-opens=java.base/java.util=ALL-UNNAMED
+
+
+
+
+
+
diff --git a/src/main/java/org/influxdb/annotation/Exclude.java b/src/main/java/org/influxdb/annotation/Exclude.java
index 01e6f52e..23e07679 100644
--- a/src/main/java/org/influxdb/annotation/Exclude.java
+++ b/src/main/java/org/influxdb/annotation/Exclude.java
@@ -28,6 +28,9 @@
/**
* When a POJO annotated with {@code @Measurement(allFields = true)} is loaded or saved,
* this annotation can be used to exclude some of its fields.
+ *
+ * Note: this is not considered when loading record measurements.
+ *
* @see Measurement#allFields()
*
* @author Eran Leshem
diff --git a/src/main/java/org/influxdb/annotation/Measurement.java b/src/main/java/org/influxdb/annotation/Measurement.java
index fa9d19fd..6ea8142e 100644
--- a/src/main/java/org/influxdb/annotation/Measurement.java
+++ b/src/main/java/org/influxdb/annotation/Measurement.java
@@ -44,6 +44,10 @@
/**
* If {@code true}, then all non-static fields of this measurement will be loaded or saved,
* regardless of any {@code @Column} annotations.
+ *
+ * Note: When loading record measurements, this is always implied to be true,
+ * since the record's canonical constructor is used to populate the record.
+ *
* @see Exclude
*/
boolean allFields() default false;
diff --git a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java
index 5a4d3af8..2cfdeced 100644
--- a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java
+++ b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java
@@ -26,21 +26,29 @@
import org.influxdb.annotation.Measurement;
import org.influxdb.dto.QueryResult;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
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 java.util.stream.Collectors;
/**
* Main class responsible for mapping a QueryResult to a POJO.
@@ -59,12 +67,26 @@ private static class ClassInfo {
private static final
ConcurrentMap CLASS_INFO_CACHE = new ConcurrentHashMap<>();
+ /**
+ * Data structure used to cache records used as measurements.
+ */
+ private static class RecordInfo {
+ Constructor> constructor;
+ ConcurrentMap constructorParamIndexes;
+ }
+ private static final
+ ConcurrentMap RECORD_INFO = 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;
+ // Support both standard and Android desugared records
+ private static final Collection RECORD_CLASS_NAMES =
+ new HashSet<>(Arrays.asList("java.lang.Record", "com.android.tools.r8.RecordTag"));
+
/**
- * When a query is executed without {@link TimeUnit}, InfluxDB returns the time
+ * When a query is executed without {@link TimeUnit}, InfluxDB returns the {@code time}
* column as a RFC3339 date.
*/
private static final DateTimeFormatter RFC3339_FORMATTER = new DateTimeFormatterBuilder()
@@ -88,7 +110,7 @@ private static class ClassInfo {
* 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
+ * {@code 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 {
@@ -111,7 +133,7 @@ public List toPOJO(final QueryResult queryResult, final Class clazz) t
* 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
+ * {@code 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,
@@ -137,7 +159,7 @@ public List toPOJO(final QueryResult queryResult, final Class clazz,
* 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
+ * {@code 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)
@@ -162,7 +184,7 @@ public List toPOJO(final QueryResult queryResult, final Class clazz, f
* 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
+ * {@code 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,
@@ -174,19 +196,20 @@ public List toPOJO(final QueryResult queryResult, final Class clazz, f
Objects.requireNonNull(clazz, "clazz");
throwExceptionIfResultWithError(queryResult);
- cacheMeasurementClass(clazz);
- List result = new LinkedList();
+ if (isRecordClass(clazz)) {
+ cacheRecordClass(clazz);
+ } else {
+ 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);
- });
- });
+ .forEach(internalResult -> internalResult.getSeries().stream()
+ .filter(series -> series.getName().equals(measurementName))
+ .forEachOrdered(series -> parseSeriesAs(series, clazz, result, precision)));
return result;
}
@@ -253,6 +276,49 @@ void cacheMeasurementClass(final Class>... classVarAgrs) {
}
}
+ static void cacheRecordClass(final Class clazz) {
+ if (RECORD_INFO.containsKey(clazz.getName())) {
+ return;
+ }
+
+ Map components = Arrays.stream(clazz.getDeclaredFields())
+ .filter(field -> !Modifier.isStatic(field.getModifiers()))
+ .collect(Collectors.toMap(Field::getName, Field::getGenericType));
+ boolean found = false;
+ for (Constructor> constructor : clazz.getDeclaredConstructors()) {
+ Parameter[] parameters = constructor.getParameters();
+ Map parameterTypes = Arrays.stream(parameters)
+ .collect(Collectors.toMap(Parameter::getName, Parameter::getParameterizedType));
+ if (!parameterTypes.equals(components)) {
+ continue;
+ }
+
+ if (found) {
+ throw new InfluxDBMapperException(String.format(
+ "Multiple constructors match set of components for record %s", clazz.getName()));
+ }
+
+ RecordInfo recordInfo = new RecordInfo();
+ recordInfo.constructor = constructor;
+
+ try {
+ ConcurrentMap constructorParamIndexes = new ConcurrentHashMap<>(parameters.length);
+ for (int i = 0; i < parameters.length; i++) {
+ Field field = clazz.getDeclaredField(parameters[i].getName());
+ Column colAnnotation = field.getAnnotation(Column.class);
+ String propertyName = getFieldName(field, colAnnotation);
+ constructorParamIndexes.put(propertyName, i);
+ }
+ recordInfo.constructorParamIndexes = constructorParamIndexes;
+ } catch (NoSuchFieldException e) {
+ throw new InfluxDBMapperException(e);
+ }
+
+ RECORD_INFO.putIfAbsent(clazz.getName(), recordInfo);
+ found = true;
+ }
+ }
+
private static String getFieldName(final Field field, final Column colAnnotation) {
if (colAnnotation != null && !colAnnotation.name().isEmpty()) {
return colAnnotation.name();
@@ -281,45 +347,86 @@ List parseSeriesAs(final QueryResult.Series series, final Class clazz,
final TimeUnit precision) {
int columnSize = series.getColumns().size();
- ClassInfo classInfo = CLASS_INFO_CACHE.get(clazz.getName());
- try {
- T object = null;
- for (List row : series.getValues()) {
- for (int i = 0; i < columnSize; i++) {
- Field correspondingField = classInfo.fieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/);
- if (correspondingField != null) {
- if (object == null) {
- object = clazz.newInstance();
+ if (isRecordClass(clazz)) {
+ RecordInfo recordInfo = RECORD_INFO.get(clazz.getName());
+ try {
+ T object = null;
+ for (List row : series.getValues()) {
+ Object[] constructorParams = new Object[recordInfo.constructor.getParameterTypes().length];
+ for (int i = 0; i < columnSize; i++) {
+ String columnName = series.getColumns().get(i); /*InfluxDB columnName*/
+ addParam(clazz, precision, recordInfo, constructorParams, columnName, row.get(i));
+ }
+ // 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) {
+ for (Entry entry : series.getTags().entrySet()) {
+ addParam(clazz, precision, recordInfo, constructorParams, entry.getKey()/*InfluxDB columnName*/,
+ entry.getValue());
}
- setFieldValue(object, correspondingField, row.get(i), precision,
- classInfo.typeMappers.get(correspondingField));
}
+
+ //noinspection unchecked
+ result.add((T) recordInfo.constructor.newInstance(constructorParams));
}
- // 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 = classInfo.fieldMap.get(entry.getKey()/*InfluxDB columnName*/);
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new InfluxDBMapperException(e);
+ }
+ } else {
+ ClassInfo classInfo = CLASS_INFO_CACHE.get(clazz.getName());
+ try {
+ T object = null;
+ for (List row : series.getValues()) {
+ for (int i = 0; i < columnSize; i++) {
+ Field correspondingField = classInfo.fieldMap.get(series.getColumns().get(i)/*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) {
+ object = clazz.newInstance();
+ }
+ setFieldValue(object, correspondingField, row.get(i), precision,
classInfo.typeMappers.get(correspondingField));
}
}
+ // 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 = classInfo.fieldMap.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,
+ classInfo.typeMappers.get(correspondingField));
+ }
+ }
+ }
+ if (object != null) {
+ result.add(object);
+ object = null;
+ }
}
- if (object != null) {
- result.add(object);
- object = null;
- }
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new InfluxDBMapperException(e);
}
- } catch (InstantiationException | IllegalAccessException e) {
- throw new InfluxDBMapperException(e);
}
return result;
}
+ private static void addParam(final Class clazz, final TimeUnit precision, final RecordInfo recordInfo,
+ final Object[] constructorParams, final String columnName, final Object value) {
+ Parameter parameter = recordInfo.constructor.getParameters()
+ [recordInfo.constructorParamIndexes.get(columnName).intValue()];
+ constructorParams[recordInfo.constructorParamIndexes.get(columnName).intValue()] =
+ adaptValue(parameter.getType(), value, precision, parameter.getName(), clazz.getName());
+ }
+
+ private static boolean isRecordClass(final Class clazz) {
+ return RECORD_CLASS_NAMES.contains(clazz.getSuperclass().getName());
+ }
+
/**
* InfluxDB client returns any number as Double.
* See ...
@@ -350,7 +457,7 @@ private static Object adaptValue(final Class> fieldType, final Object value, f
return Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value)));
}
if (value instanceof Long) {
- return Instant.ofEpochMilli(toMillis((long) value, precision));
+ return Instant.ofEpochMilli(toMillis(((Long) value).longValue(), precision));
}
if (value instanceof Double) {
return Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision));
@@ -379,8 +486,7 @@ private static Object adaptValue(final Class> fieldType, final Object value, f
} 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, className, fieldName, value.getClass().getName(), value));
+ throw new InfluxDBMapperException(String.format(msg, className, fieldName, value.getClass().getName(), value), e);
}
throw new InfluxDBMapperException(
diff --git a/src/test-jdk17/java/org/influxdb/impl/InfluxDBRecordResultMapperTest.java b/src/test-jdk17/java/org/influxdb/impl/InfluxDBRecordResultMapperTest.java
new file mode 100644
index 00000000..c16fa24b
--- /dev/null
+++ b/src/test-jdk17/java/org/influxdb/impl/InfluxDBRecordResultMapperTest.java
@@ -0,0 +1,579 @@
+/*
+ * 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 org.influxdb.InfluxDBMapperException;
+import org.influxdb.annotation.Column;
+import org.influxdb.annotation.Measurement;
+import org.influxdb.annotation.TimeColumn;
+import org.influxdb.dto.QueryResult;
+import org.junit.Assert;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Eran Leshem
+ */
+@SuppressWarnings({"removal", "deprecation"})
+@RunWith(JUnitPlatform.class)
+public class InfluxDBRecordResultMapperTest {
+
+ private final InfluxDBResultMapper mapper = new InfluxDBResultMapper();
+
+ @Test
+ public void testToRecord_HappyPath() {
+ // Given...
+ var columnList = Arrays.asList("time", "uuid");
+ List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString());
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ series.setName("CustomMeasurement");
+ series.setValues(List.of(firstSeriesResult));
+
+ var internalResult = new QueryResult.Result();
+ internalResult.setSeries(List.of(series));
+
+ var queryResult = new QueryResult();
+ queryResult.setResults(List.of(internalResult));
+
+ //When...
+ var myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class);
+
+ // Then...
+ Assertions.assertEquals(1, myList.size(), "there must be one entry in the result list");
+ }
+
+ @Test
+ public void testThrowExceptionIfMissingAnnotation() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> mapper.throwExceptionIfMissingAnnotation(String.class));
+ }
+
+ @Test
+ public void testThrowExceptionIfError_InfluxQueryResultHasError() {
+ var queryResult = new QueryResult();
+ queryResult.setError("main queryresult error");
+
+ Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult));
+ }
+
+ @Test
+ public void testThrowExceptionIfError_InfluxQueryResultSeriesHasError() {
+ var seriesResult = new QueryResult.Result();
+ seriesResult.setError("series error");
+
+ var queryResult = new QueryResult();
+ queryResult.setResults(List.of(seriesResult));
+
+ Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult));
+ }
+
+ @Test
+ public void testGetMeasurementName_testStateMeasurement() {
+ Assertions.assertEquals("CustomMeasurement", mapper.getMeasurementName(MyCustomMeasurement.class));
+ }
+
+ @Test
+ public void testParseSeriesAs_testTwoValidSeries() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var columnList = Arrays.asList("time", "uuid");
+
+ List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString());
+ List secondSeriesResult = Arrays.asList(Instant.now().plusSeconds(1).toEpochMilli(),
+ UUID.randomUUID().toString());
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ series.setValues(Arrays.asList(firstSeriesResult, secondSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 2, "there must be two series in the result list");
+
+ Assertions.assertEquals(firstSeriesResult.get(0), result.get(0).time().toEpochMilli(),
+ "Field 'time' (1st series) is not valid");
+ Assertions.assertEquals(firstSeriesResult.get(1), result.get(0).uuid(), "Field 'uuid' (1st series) is not valid");
+
+ Assertions.assertEquals(secondSeriesResult.get(0), result.get(1).time().toEpochMilli(),
+ "Field 'time' (2nd series) is not valid");
+ Assertions.assertEquals(secondSeriesResult.get(1), result.get(1).uuid(), "Field 'uuid' (2nd series) is not valid");
+ }
+
+ @Test
+ public void testParseSeriesAs_testNonNullAndValidValues() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurementWithPrimitives.class);
+
+ var columnList = Arrays.asList("time", "uuid",
+ "doubleObject", "longObject", "integerObject",
+ "doublePrimitive", "longPrimitive", "integerPrimitive",
+ "booleanObject", "booleanPrimitive");
+
+ // InfluxDB client returns the time representation as Double.
+ Double now = Long.valueOf(System.currentTimeMillis()).doubleValue();
+
+ // InfluxDB client returns any number as Double.
+ // See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987
+ // for more information.
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ var uuidAsString = UUID.randomUUID().toString();
+ List seriesResult = Arrays.asList(now, uuidAsString,
+ new Double("1.01"), new Double("2"), new Double("3"),
+ new Double("1.01"), new Double("4"), new Double("5"),
+ "false", "true");
+ series.setValues(List.of(seriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurementWithPrimitives.class, result);
+
+ //Then...
+ var myObject = result.get(0);
+ Assertions.assertEquals(now.longValue(), myObject.time().toEpochMilli(), "field 'time' does not match");
+ Assertions.assertEquals(uuidAsString, myObject.uuid(), "field 'uuid' does not match");
+
+ Assertions.assertEquals(asDouble(seriesResult.get(2)), myObject.doubleObject(),
+ "field 'doubleObject' does not match");
+ Assertions.assertEquals(Long.valueOf(asDouble(seriesResult.get(3)).longValue()), myObject.longObject(),
+ "field 'longObject' does not match");
+ Assertions.assertEquals(Integer.valueOf(asDouble(seriesResult.get(4)).intValue()), myObject.integerObject(),
+ "field 'integerObject' does not match");
+
+ Assertions.assertTrue(
+ Double.compare(asDouble(seriesResult.get(5)).doubleValue(), myObject.doublePrimitive()) == 0,
+ "field 'doublePrimitive' does not match");
+
+ Assertions.assertTrue(asDouble(seriesResult.get(6)).longValue() == myObject.longPrimitive(),
+ "field 'longPrimitive' does not match");
+
+ Assertions.assertTrue(asDouble(seriesResult.get(7)).intValue() == myObject.integerPrimitive(),
+ "field 'integerPrimitive' does not match");
+
+ Assertions.assertEquals(
+ Boolean.valueOf(String.valueOf(seriesResult.get(8))), myObject.booleanObject(),
+ "field 'booleanObject' does not match");
+
+ Assertions.assertEquals(
+ Boolean.valueOf(String.valueOf(seriesResult.get(9))).booleanValue(), myObject.booleanPrimitive(),
+ "field 'booleanPrimitive' does not match");
+ }
+
+ private static Double asDouble(Object obj) {
+ return (Double) obj;
+ }
+
+ @Test
+ public void testFieldValueModified_DateAsISO8601() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var columnList = List.of("time");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = List.of("2017-06-19T09:29:45.655123Z");
+ series.setValues(List.of(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+ }
+
+ @Test
+ public void testFieldValueModified_DateAsInteger() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var columnList = List.of("time");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = List.of(1_000);
+ series.setValues(List.of(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+ }
+
+ @Test
+ public void testUnsupportedField() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyRecordWithUnsupportedField.class);
+
+ var columnList = List.of("bar");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = List.of("content representing a Date");
+ series.setValues(List.of(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ Assertions.assertThrows(InfluxDBMapperException.class,
+ () -> mapper.parseSeriesAs(series, MyRecordWithUnsupportedField.class, result));
+ }
+
+ /**
+ * for more information .
+ */
+ @Test
+ public void testToRecord_SeriesFromQueryResultIsNull() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var internalResult = new QueryResult.Result();
+ internalResult.setSeries(null);
+
+ var queryResult = new QueryResult();
+ queryResult.setResults(List.of(internalResult));
+
+ // When...
+ var myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class);
+
+ // Then...
+ Assertions.assertTrue( myList.isEmpty(), "there must NO entry in the result list");
+ }
+
+ @Test
+ public void testToRecord_QueryResultCreatedByGroupByClause() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(GroupByCarrierDeviceOS.class);
+
+ // InfluxDB client returns the time representation as Double.
+ Double now = Long.valueOf(System.currentTimeMillis()).doubleValue();
+
+ // When the "GROUP BY" clause is used, "tags" are returned as Map
+ Map firstSeriesTagMap = new HashMap<>(2);
+ firstSeriesTagMap.put("CARRIER", "000/00");
+ firstSeriesTagMap.put("DEVICE_OS_VERSION", "4.4.2");
+
+ Map secondSeriesTagMap = new HashMap<>(2);
+ secondSeriesTagMap.put("CARRIER", "000/01");
+ secondSeriesTagMap.put("DEVICE_OS_VERSION", "9.3.5");
+
+ var firstSeries = new QueryResult.Series();
+ var columnList = Arrays.asList("time", "median", "min", "max");
+ firstSeries.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList(now, new Double("233.8"), new Double("0.0"),
+ new Double("3090744.0"));
+ firstSeries.setValues(List.of(firstSeriesResult));
+ firstSeries.setTags(firstSeriesTagMap);
+ firstSeries.setName("tb_network");
+
+ var secondSeries = new QueryResult.Series();
+ secondSeries.setColumns(columnList);
+ List secondSeriesResult = Arrays.asList(now, new Double("552.0"), new Double("135.0"),
+ new Double("267705.0"));
+ secondSeries.setValues(List.of(secondSeriesResult));
+ secondSeries.setTags(secondSeriesTagMap);
+ secondSeries.setName("tb_network");
+
+ var internalResult = new QueryResult.Result();
+ internalResult.setSeries(Arrays.asList(firstSeries, secondSeries));
+
+ var queryResult = new QueryResult();
+ queryResult.setResults(List.of(internalResult));
+
+ // When...
+ var myList = mapper.toPOJO(queryResult, GroupByCarrierDeviceOS.class);
+
+ // Then...
+ var firstGroupByEntry = myList.get(0);
+ Assertions.assertEquals("000/00", firstGroupByEntry.carrier(), "field 'carrier' does not match");
+ Assertions.assertEquals("4.4.2", firstGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match");
+
+ var secondGroupByEntry = myList.get(1);
+ Assertions.assertEquals("000/01", secondGroupByEntry.carrier(), "field 'carrier' does not match");
+ Assertions.assertEquals("9.3.5", secondGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match");
+ }
+
+ @Test
+ public void testToRecord_ticket363() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var columnList = List.of("time");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = List.of("2000-01-01T00:00:00.000000001Z");
+ series.setValues(List.of(firstSeriesResult));
+
+ // When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ // Then...
+ Assertions.assertEquals(1, result.size(), "incorrect number of elemets");
+ Assertions.assertEquals(1, result.get(0).time().getNano(), "incorrect value for the nanoseconds field");
+ }
+
+ @Test
+ void testToRecord_Precision() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var series = new QueryResult.Series();
+ series.setName("CustomMeasurement");
+ var columnList = List.of("time");
+ series.setColumns(columnList);
+ List firstSeriesResult = List.of(1_500_000L);
+ series.setValues(List.of(firstSeriesResult));
+
+ var internalResult = new QueryResult.Result();
+ internalResult.setSeries(List.of(series));
+
+ var queryResult = new QueryResult();
+ queryResult.setResults(List.of(internalResult));
+
+ // When...
+ var result = mapper.toPOJO(queryResult, MyCustomMeasurement.class, TimeUnit.SECONDS);
+
+ // Then...
+ Assertions.assertEquals(1, result.size(), "incorrect number of elements");
+ Assertions.assertEquals(1_500_000_000L, result.get(0).time().toEpochMilli(),
+ "incorrect value for the millis field");
+ }
+
+ @Test
+ void testToRecord_SetMeasureName() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var series = new QueryResult.Series();
+ series.setName("MySeriesName");
+ var columnList = List.of("uuid");
+ series.setColumns(columnList);
+ List firstSeriesResult = Collections.singletonList(UUID.randomUUID().toString());
+ series.setValues(List.of(firstSeriesResult));
+
+ var internalResult = new QueryResult.Result();
+ internalResult.setSeries(List.of(series));
+
+ var queryResult = new QueryResult();
+ queryResult.setResults(List.of(internalResult));
+
+ //When...
+ var result =
+ mapper.toPOJO(queryResult, MyCustomMeasurement.class, "MySeriesName");
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+ }
+
+ @Test
+ public void testToRecord_HasTimeColumn() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(HasTimeColumnMeasurement.class);
+
+ var columnList = List.of("time");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List> valuesList = Arrays.asList(
+ List.of("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5)
+ List.of("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5)
+ List.of("2000-01-01T00:00:00-00:00"),
+ List.of("2000-01-02T00:00:00+00:00")
+ );
+ series.setValues(valuesList);
+
+ // When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, HasTimeColumnMeasurement.class, result);
+
+ // Then...
+ Assertions.assertEquals(4, result.size(), "incorrect number of elemets");
+ // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC)
+ Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z")));
+ Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z")));
+ // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3
+ Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z")));
+ Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z")));
+
+ }
+
+ @Test
+ public void testToRecord_ticket573() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ var columnList = List.of("time");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List> valuesList = Arrays.asList(
+ List.of("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5)
+ List.of("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5)
+ List.of("2000-01-01T00:00:00-00:00"),
+ List.of("2000-01-02T00:00:00+00:00")
+ );
+ series.setValues(valuesList);
+
+ // When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ // Then...
+ Assertions.assertEquals(4, result.size(), "incorrect number of elemets");
+ // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC)
+ Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z")));
+ Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z")));
+ // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3
+ Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z")));
+ Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z")));
+ }
+
+ @Test
+ public void testMultipleConstructors() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MultipleConstructors.class);
+
+ var columnList = List.of("i", "s");
+
+ var series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = List.of(9.0, "str");
+ series.setValues(List.of(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MultipleConstructors.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+
+ Assert.assertEquals(9, result.get(0).i());
+ Assert.assertEquals("str", result.get(0).s());
+ }
+
+ @Test
+ public void testConflictingConstructors() {
+ Assert.assertThrows(InfluxDBMapperException.class,
+ () -> InfluxDBResultMapper.cacheRecordClass(ConflictingConstructors.class));
+ }
+
+ @Measurement(name = "HasTimeColumnMeasurement")
+ record HasTimeColumnMeasurement(
+ @TimeColumn
+ Instant time,
+ Integer value) {}
+
+ @Measurement(name = "CustomMeasurement")
+ record MyCustomMeasurement(
+ Instant time,
+ String uuid,
+ Double doubleObject,
+ Long longObject,
+ Integer integerObject,
+ Boolean booleanObject,
+
+ @SuppressWarnings("unused")
+ String nonColumn1,
+
+ @SuppressWarnings("unused")
+ Random rnd) {}
+
+ @Measurement(name = "CustomMeasurement")
+ record MyCustomMeasurementWithPrimitives(
+ Instant time,
+ String uuid,
+ Double doubleObject,
+ Long longObject,
+ Integer integerObject,
+ double doublePrimitive,
+ long longPrimitive,
+ int integerPrimitive,
+ Boolean booleanObject,
+ boolean booleanPrimitive,
+
+ @SuppressWarnings("unused")
+ String nonColumn1,
+
+ @SuppressWarnings("unused")
+ Random rnd) {}
+
+ @Measurement(name = "foo")
+ record MyRecordWithUnsupportedField(
+ @Column(name = "bar")
+ Date myDate) {}
+
+ /**
+ * Class created based on example from this issue
+ */
+ @Measurement(name = "tb_network")
+ record GroupByCarrierDeviceOS(
+ Instant time,
+
+ @Column(name = "CARRIER", tag = true)
+ String carrier,
+
+ @Column(name = "DEVICE_OS_VERSION", tag = true)
+ String deviceOsVersion,
+
+ Double median,
+ Double min,
+ Double max) {}
+
+ record MultipleConstructors(int i, String s) {
+ MultipleConstructors(String i, String s) {
+ this(Integer.parseInt(i), s);
+ }
+
+ MultipleConstructors(int i, String s, double d) {
+ this(i, s);
+ }
+ }
+
+ record ConflictingConstructors(int i, String s) {
+ private ConflictingConstructors(String s, int i) {
+ this(i, s);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/RecordTag.java b/src/test/java/com/android/tools/r8/RecordTag.java
new file mode 100644
index 00000000..51aacedb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/RecordTag.java
@@ -0,0 +1,9 @@
+package com.android.tools.r8;
+
+/**
+ * Simulates the super class of Android-desugared records.
+ *
+ * @author Eran Leshem
+ **/
+public class RecordTag {
+}
diff --git a/src/test/java/org/influxdb/impl/InfluxDBAndroidDesugaredRecordResultMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBAndroidDesugaredRecordResultMapperTest.java
new file mode 100644
index 00000000..f1dc21dc
--- /dev/null
+++ b/src/test/java/org/influxdb/impl/InfluxDBAndroidDesugaredRecordResultMapperTest.java
@@ -0,0 +1,819 @@
+/*
+ * 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 com.android.tools.r8.RecordTag;
+import org.influxdb.InfluxDBMapperException;
+import org.influxdb.annotation.Column;
+import org.influxdb.annotation.Measurement;
+import org.influxdb.annotation.TimeColumn;
+import org.influxdb.dto.QueryResult;
+import org.junit.Assert;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test measurement classes simulate Android desugared records.
+ *
+ * @author Eran Leshem
+ */
+@SuppressWarnings({"removal", "deprecation"})
+@RunWith(JUnitPlatform.class)
+public class InfluxDBAndroidDesugaredRecordResultMapperTest {
+
+ private final InfluxDBResultMapper mapper = new InfluxDBResultMapper();
+
+ @Test
+ public void testToRecord_HappyPath() {
+ // Given...
+ List columnList = Arrays.asList("time", "uuid");
+ List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString());
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ series.setName("CustomMeasurement");
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ QueryResult.Result internalResult = new QueryResult.Result();
+ internalResult.setSeries(Arrays.asList(series));
+
+ QueryResult queryResult = new QueryResult();
+ queryResult.setResults(Arrays.asList(internalResult));
+
+ //When...
+ List myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class);
+
+ // Then...
+ Assertions.assertEquals(1, myList.size(), "there must be one entry in the result list");
+ }
+
+ @Test
+ public void testThrowExceptionIfMissingAnnotation() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> mapper.throwExceptionIfMissingAnnotation(String.class));
+ }
+
+ @Test
+ public void testThrowExceptionIfError_InfluxQueryResultHasError() {
+ QueryResult queryResult = new QueryResult();
+ queryResult.setError("main queryresult error");
+
+ Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult));
+ }
+
+ @Test
+ public void testThrowExceptionIfError_InfluxQueryResultSeriesHasError() {
+ QueryResult.Result seriesResult = new QueryResult.Result();
+ seriesResult.setError("series error");
+
+ QueryResult queryResult = new QueryResult();
+ queryResult.setResults(Arrays.asList(seriesResult));
+
+ Assertions.assertThrows(InfluxDBMapperException.class, () -> mapper.throwExceptionIfResultWithError(queryResult));
+ }
+
+ @Test
+ public void testGetMeasurementName_testStateMeasurement() {
+ Assertions.assertEquals("CustomMeasurement", mapper.getMeasurementName(MyCustomMeasurement.class));
+ }
+
+ @Test
+ public void testParseSeriesAs_testTwoValidSeries() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ List columnList = Arrays.asList("time", "uuid");
+
+ List firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString());
+ List secondSeriesResult = Arrays.asList(Instant.now().plusSeconds(1).toEpochMilli(),
+ UUID.randomUUID().toString());
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ series.setValues(Arrays.asList(firstSeriesResult, secondSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 2, "there must be two series in the result list");
+
+ Assertions.assertEquals(firstSeriesResult.get(0), result.get(0).time().toEpochMilli(),
+ "Field 'time' (1st series) is not valid");
+ Assertions.assertEquals(firstSeriesResult.get(1), result.get(0).uuid(), "Field 'uuid' (1st series) is not valid");
+
+ Assertions.assertEquals(secondSeriesResult.get(0), result.get(1).time().toEpochMilli(),
+ "Field 'time' (2nd series) is not valid");
+ Assertions.assertEquals(secondSeriesResult.get(1), result.get(1).uuid(), "Field 'uuid' (2nd series) is not valid");
+ }
+
+ @Test
+ public void testParseSeriesAs_testNonNullAndValidValues() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurementWithPrimitives.class);
+
+ List columnList = Arrays.asList("time", "uuid",
+ "doubleObject", "longObject", "integerObject",
+ "doublePrimitive", "longPrimitive", "integerPrimitive",
+ "booleanObject", "booleanPrimitive");
+
+ // InfluxDB client returns the time representation as Double.
+ Double now = Long.valueOf(System.currentTimeMillis()).doubleValue();
+
+ // InfluxDB client returns any number as Double.
+ // See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987
+ // for more information.
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ String uuidAsString = UUID.randomUUID().toString();
+ List seriesResult = Arrays.asList(now, uuidAsString,
+ new Double("1.01"), new Double("2"), new Double("3"),
+ new Double("1.01"), new Double("4"), new Double("5"),
+ "false", "true");
+ series.setValues(Arrays.asList(seriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurementWithPrimitives.class, result);
+
+ //Then...
+ MyCustomMeasurementWithPrimitives myObject = result.get(0);
+ Assertions.assertEquals(now.longValue(), myObject.time().toEpochMilli(), "field 'time' does not match");
+ Assertions.assertEquals(uuidAsString, myObject.uuid(), "field 'uuid' does not match");
+
+ Assertions.assertEquals(asDouble(seriesResult.get(2)), myObject.doubleObject(),
+ "field 'doubleObject' does not match");
+ Assertions.assertEquals(Long.valueOf(asDouble(seriesResult.get(3)).longValue()), myObject.longObject(),
+ "field 'longObject' does not match");
+ Assertions.assertEquals(Integer.valueOf(asDouble(seriesResult.get(4)).intValue()), myObject.integerObject(),
+ "field 'integerObject' does not match");
+
+ Assertions.assertTrue(
+ Double.compare(asDouble(seriesResult.get(5)).doubleValue(), myObject.doublePrimitive()) == 0,
+ "field 'doublePrimitive' does not match");
+
+ Assertions.assertTrue(asDouble(seriesResult.get(6)).longValue() == myObject.longPrimitive(),
+ "field 'longPrimitive' does not match");
+
+ Assertions.assertTrue(asDouble(seriesResult.get(7)).intValue() == myObject.integerPrimitive(),
+ "field 'integerPrimitive' does not match");
+
+ Assertions.assertEquals(
+ Boolean.valueOf(String.valueOf(seriesResult.get(8))), myObject.booleanObject(),
+ "field 'booleanObject' does not match");
+
+ Assertions.assertEquals(
+ Boolean.valueOf(String.valueOf(seriesResult.get(9))).booleanValue(), myObject.booleanPrimitive(),
+ "field 'booleanPrimitive' does not match");
+ }
+
+ private static Double asDouble(Object obj) {
+ return (Double) obj;
+ }
+
+ @Test
+ public void testFieldValueModified_DateAsISO8601() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ List columnList = Arrays.asList("time");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList("2017-06-19T09:29:45.655123Z");
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+ }
+
+ @Test
+ public void testFieldValueModified_DateAsInteger() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ List columnList = Arrays.asList("time");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList(1_000);
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+ }
+
+ @Test
+ public void testUnsupportedField() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyRecordWithUnsupportedField.class);
+
+ List columnList = Arrays.asList("bar");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList("content representing a Date");
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ Assertions.assertThrows(InfluxDBMapperException.class,
+ () -> mapper.parseSeriesAs(series, MyRecordWithUnsupportedField.class, result));
+ }
+
+ /**
+ * for more information .
+ */
+ @Test
+ public void testToRecord_SeriesFromQueryResultIsNull() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ QueryResult.Result internalResult = new QueryResult.Result();
+ internalResult.setSeries(null);
+
+ QueryResult queryResult = new QueryResult();
+ queryResult.setResults(Arrays.asList(internalResult));
+
+ // When...
+ List myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class);
+
+ // Then...
+ Assertions.assertTrue(myList.isEmpty(), "there must NO entry in the result list");
+ }
+
+ @Test
+ public void testToRecord_QueryResultCreatedByGroupByClause() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(GroupByCarrierDeviceOS.class);
+
+ // InfluxDB client returns the time representation as Double.
+ Double now = Long.valueOf(System.currentTimeMillis()).doubleValue();
+
+ // When the "GROUP BY" clause is used, "tags" are returned as Map
+ Map firstSeriesTagMap = new HashMap<>(2);
+ firstSeriesTagMap.put("CARRIER", "000/00");
+ firstSeriesTagMap.put("DEVICE_OS_VERSION", "4.4.2");
+
+ Map secondSeriesTagMap = new HashMap<>(2);
+ secondSeriesTagMap.put("CARRIER", "000/01");
+ secondSeriesTagMap.put("DEVICE_OS_VERSION", "9.3.5");
+
+ QueryResult.Series firstSeries = new QueryResult.Series();
+ List columnList = Arrays.asList("time", "median", "min", "max");
+ firstSeries.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList(now, new Double("233.8"), new Double("0.0"),
+ new Double("3090744.0"));
+ firstSeries.setValues(Arrays.asList(firstSeriesResult));
+ firstSeries.setTags(firstSeriesTagMap);
+ firstSeries.setName("tb_network");
+
+ QueryResult.Series secondSeries = new QueryResult.Series();
+ secondSeries.setColumns(columnList);
+ List secondSeriesResult = Arrays.asList(now, new Double("552.0"), new Double("135.0"),
+ new Double("267705.0"));
+ secondSeries.setValues(Arrays.asList(secondSeriesResult));
+ secondSeries.setTags(secondSeriesTagMap);
+ secondSeries.setName("tb_network");
+
+ QueryResult.Result internalResult = new QueryResult.Result();
+ internalResult.setSeries(Arrays.asList(firstSeries, secondSeries));
+
+ QueryResult queryResult = new QueryResult();
+ queryResult.setResults(Arrays.asList(internalResult));
+
+ // When...
+ List myList = mapper.toPOJO(queryResult, GroupByCarrierDeviceOS.class);
+
+ // Then...
+ GroupByCarrierDeviceOS firstGroupByEntry = myList.get(0);
+ Assertions.assertEquals("000/00", firstGroupByEntry.carrier(), "field 'carrier' does not match");
+ Assertions.assertEquals("4.4.2", firstGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match");
+
+ GroupByCarrierDeviceOS secondGroupByEntry = myList.get(1);
+ Assertions.assertEquals("000/01", secondGroupByEntry.carrier(), "field 'carrier' does not match");
+ Assertions.assertEquals("9.3.5", secondGroupByEntry.deviceOsVersion(), "field 'deviceOsVersion' does not match");
+ }
+
+ @Test
+ public void testToRecord_ticket363() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ List columnList = Arrays.asList("time");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList("2000-01-01T00:00:00.000000001Z");
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ // When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ // Then...
+ Assertions.assertEquals(1, result.size(), "incorrect number of elemets");
+ Assertions.assertEquals(1, result.get(0).time().getNano(), "incorrect value for the nanoseconds field");
+ }
+
+ @Test
+ void testToRecord_Precision() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setName("CustomMeasurement");
+ List columnList = Arrays.asList("time");
+ series.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList(1_500_000L);
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ QueryResult.Result internalResult = new QueryResult.Result();
+ internalResult.setSeries(Arrays.asList(series));
+
+ QueryResult queryResult = new QueryResult();
+ queryResult.setResults(Arrays.asList(internalResult));
+
+ // When...
+ List result = mapper.toPOJO(queryResult, MyCustomMeasurement.class, TimeUnit.SECONDS);
+
+ // Then...
+ Assertions.assertEquals(1, result.size(), "incorrect number of elements");
+ Assertions.assertEquals(1_500_000_000L, result.get(0).time().toEpochMilli(),
+ "incorrect value for the millis field");
+ }
+
+ @Test
+ void testToRecord_SetMeasureName() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setName("MySeriesName");
+ List columnList = Arrays.asList("uuid");
+ series.setColumns(columnList);
+ List firstSeriesResult = Collections.singletonList(UUID.randomUUID().toString());
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ QueryResult.Result internalResult = new QueryResult.Result();
+ internalResult.setSeries(Arrays.asList(series));
+
+ QueryResult queryResult = new QueryResult();
+ queryResult.setResults(Arrays.asList(internalResult));
+
+ //When...
+ List result =
+ mapper.toPOJO(queryResult, MyCustomMeasurement.class, "MySeriesName");
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+ }
+
+ @Test
+ public void testToRecord_HasTimeColumn() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(HasTimeColumnMeasurement.class);
+
+ List columnList = Arrays.asList("time");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List> valuesList = Arrays.asList(
+ Arrays.asList("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5)
+ Arrays.asList("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5)
+ Arrays.asList("2000-01-01T00:00:00-00:00"),
+ Arrays.asList("2000-01-02T00:00:00+00:00")
+ );
+ series.setValues(valuesList);
+
+ // When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, HasTimeColumnMeasurement.class, result);
+
+ // Then...
+ Assertions.assertEquals(4, result.size(), "incorrect number of elemets");
+ // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC)
+ Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z")));
+ Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z")));
+ // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3
+ Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z")));
+ Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z")));
+
+ }
+
+ @Test
+ public void testToRecord_ticket573() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MyCustomMeasurement.class);
+
+ List columnList = Arrays.asList("time");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List> valuesList = Arrays.asList(
+ Arrays.asList("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5)
+ Arrays.asList("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5)
+ Arrays.asList("2000-01-01T00:00:00-00:00"),
+ Arrays.asList("2000-01-02T00:00:00+00:00")
+ );
+ series.setValues(valuesList);
+
+ // When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
+
+ // Then...
+ Assertions.assertEquals(4, result.size(), "incorrect number of elemets");
+ // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC)
+ Assertions.assertTrue(result.get(0).time().equals(Instant.parse("2015-08-18T00:00:00Z")));
+ Assertions.assertTrue(result.get(1).time().equals(Instant.parse("2015-08-18T00:00:00.000000001Z")));
+ // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3
+ Assertions.assertTrue(result.get(2).time().equals(Instant.parse("2000-01-01T00:00:00Z")));
+ Assertions.assertTrue(result.get(3).time().equals(Instant.parse("2000-01-02T00:00:00Z")));
+ }
+
+ @Test
+ public void testMultipleConstructors() {
+ // Given...
+ InfluxDBResultMapper.cacheRecordClass(MultipleConstructors.class);
+
+ List columnList = Arrays.asList("i", "s");
+
+ QueryResult.Series series = new QueryResult.Series();
+ series.setColumns(columnList);
+ List firstSeriesResult = Arrays.asList(9.0, "str");
+ series.setValues(Arrays.asList(firstSeriesResult));
+
+ //When...
+ List result = new LinkedList<>();
+ mapper.parseSeriesAs(series, MultipleConstructors.class, result);
+
+ //Then...
+ Assertions.assertTrue(result.size() == 1);
+
+ Assert.assertEquals(9, result.get(0).i());
+ Assert.assertEquals("str", result.get(0).s());
+ }
+
+ @Test
+ public void testConflictingConstructors() {
+ Assert.assertThrows(InfluxDBMapperException.class,
+ () -> InfluxDBResultMapper.cacheRecordClass(ConflictingConstructors.class));
+ }
+
+ @Measurement(name = "HasTimeColumnMeasurement")
+ static final class HasTimeColumnMeasurement extends RecordTag {
+ @TimeColumn
+ private final Instant time;
+ private final Integer value;
+
+ HasTimeColumnMeasurement(Instant time, Integer value) {
+ this.time = time;
+ this.value = value;
+ }
+
+ public Instant time() {
+ return time;
+ }
+
+ public Integer value() {
+ return value;
+ }
+ }
+
+ @Measurement(name = "CustomMeasurement")
+ static final class MyCustomMeasurement extends RecordTag {
+ private final Instant time;
+ private final String uuid;
+ private final Double doubleObject;
+ private final Long longObject;
+ private final Integer integerObject;
+ private final Boolean booleanObject;
+
+ @SuppressWarnings("unused")
+ private final String nonColumn1;
+
+ @SuppressWarnings("unused")
+ private final Random rnd;
+
+ MyCustomMeasurement(
+ Instant time,
+ String uuid,
+ Double doubleObject,
+ Long longObject,
+ Integer integerObject,
+ Boolean booleanObject,
+
+ @SuppressWarnings("unused")
+ String nonColumn1,
+
+ @SuppressWarnings("unused")
+ Random rnd) {
+ this.time = time;
+ this.uuid = uuid;
+ this.doubleObject = doubleObject;
+ this.longObject = longObject;
+ this.integerObject = integerObject;
+ this.booleanObject = booleanObject;
+ this.nonColumn1 = nonColumn1;
+ this.rnd = rnd;
+ }
+
+ public Instant time() {
+ return time;
+ }
+
+ public String uuid() {
+ return uuid;
+ }
+
+ public Double doubleObject() {
+ return doubleObject;
+ }
+
+ public Long longObject() {
+ return longObject;
+ }
+
+ public Integer integerObject() {
+ return integerObject;
+ }
+
+ public Boolean booleanObject() {
+ return booleanObject;
+ }
+
+ @SuppressWarnings("unused")
+ public String nonColumn1() {
+ return nonColumn1;
+ }
+
+ @SuppressWarnings("unused")
+ public Random rnd() {
+ return rnd;
+ }
+ }
+
+ @Measurement(name = "CustomMeasurement")
+ static final class MyCustomMeasurementWithPrimitives extends RecordTag {
+ private final Instant time;
+ private final String uuid;
+ private final Double doubleObject;
+ private final Long longObject;
+ private final Integer integerObject;
+ private final double doublePrimitive;
+ private final long longPrimitive;
+ private final int integerPrimitive;
+ private final Boolean booleanObject;
+ private final boolean booleanPrimitive;
+
+ @SuppressWarnings("unused")
+ private final String nonColumn1;
+
+ @SuppressWarnings("unused")
+ private final Random rnd;
+
+ MyCustomMeasurementWithPrimitives(
+ Instant time,
+ String uuid,
+ Double doubleObject,
+ Long longObject,
+ Integer integerObject,
+ double doublePrimitive,
+ long longPrimitive,
+ int integerPrimitive,
+ Boolean booleanObject,
+ boolean booleanPrimitive,
+
+ @SuppressWarnings("unused")
+ String nonColumn1,
+
+ @SuppressWarnings("unused")
+ Random rnd) {
+ this.time = time;
+ this.uuid = uuid;
+ this.doubleObject = doubleObject;
+ this.longObject = longObject;
+ this.integerObject = integerObject;
+ this.doublePrimitive = doublePrimitive;
+ this.longPrimitive = longPrimitive;
+ this.integerPrimitive = integerPrimitive;
+ this.booleanObject = booleanObject;
+ this.booleanPrimitive = booleanPrimitive;
+ this.nonColumn1 = nonColumn1;
+ this.rnd = rnd;
+ }
+
+ public Instant time() {
+ return time;
+ }
+
+ public String uuid() {
+ return uuid;
+ }
+
+ public Double doubleObject() {
+ return doubleObject;
+ }
+
+ public Long longObject() {
+ return longObject;
+ }
+
+ public Integer integerObject() {
+ return integerObject;
+ }
+
+ public double doublePrimitive() {
+ return doublePrimitive;
+ }
+
+ public long longPrimitive() {
+ return longPrimitive;
+ }
+
+ public int integerPrimitive() {
+ return integerPrimitive;
+ }
+
+ public Boolean booleanObject() {
+ return booleanObject;
+ }
+
+ public boolean booleanPrimitive() {
+ return booleanPrimitive;
+ }
+
+ @SuppressWarnings("unused")
+ public String nonColumn1() {
+ return nonColumn1;
+ }
+
+ @SuppressWarnings("unused")
+ public Random rnd() {
+ return rnd;
+ }
+ }
+
+ @Measurement(name = "foo")
+ static final class MyRecordWithUnsupportedField extends RecordTag {
+ @Column(name = "bar")
+ private final Date myDate;
+
+ MyRecordWithUnsupportedField(Date myDate) {
+ this.myDate = myDate;
+ }
+
+ public Date myDate() {
+ return myDate;
+ }
+ }
+
+ /**
+ * Class created based on example from this issue
+ */
+ @Measurement(name = "tb_network")
+ static final class GroupByCarrierDeviceOS extends RecordTag {
+ private final Instant time;
+
+ @Column(name = "CARRIER", tag = true)
+ private final String carrier;
+
+ @Column(name = "DEVICE_OS_VERSION", tag = true)
+ private final String deviceOsVersion;
+
+ private final Double median;
+ private final Double min;
+ private final Double max;
+
+ GroupByCarrierDeviceOS(
+ Instant time,
+ String carrier,
+ String deviceOsVersion,
+ Double median,
+ Double min,
+ Double max) {
+ this.time = time;
+ this.carrier = carrier;
+ this.deviceOsVersion = deviceOsVersion;
+ this.median = median;
+ this.min = min;
+ this.max = max;
+ }
+
+ public Instant time() {
+ return time;
+ }
+
+ public String carrier() {
+ return carrier;
+ }
+
+ public String deviceOsVersion() {
+ return deviceOsVersion;
+ }
+
+ public Double median() {
+ return median;
+ }
+
+ public Double min() {
+ return min;
+ }
+
+ public Double max() {
+ return max;
+ }
+ }
+
+ static final class MultipleConstructors extends RecordTag {
+ private final int i;
+ private final String s;
+
+ MultipleConstructors(int i, String s) {
+ this.i = i;
+ this.s = s;
+ }
+
+ MultipleConstructors(String i, String s) {
+ this(Integer.parseInt(i), s);
+ }
+
+ MultipleConstructors(int i, String s, double d) {
+ this(i, s);
+ }
+
+ int i() {
+ return i;
+ }
+
+ String s() {
+ return s;
+ }
+ }
+
+
+ static final class ConflictingConstructors extends RecordTag {
+ private final int i;
+ private final String s;
+
+ private ConflictingConstructors(int i, String s) {
+ this.i = i;
+ this.s = s;
+ }
+
+ private ConflictingConstructors(String s, int i) {
+ this(i, s);
+ }
+
+ public int i() {
+ return i;
+ }
+
+ public String s() {
+ return s;
+ }
+ }
+}
\ No newline at end of file