diff --git a/README.md b/README.md index 81d44674d..3c10745e9 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ public void write(final int udpPort, final Point point); note: make sure write content's total size should not > UDP protocol's limit(64K), or you should use http instead of udp. -#### chunking support (version 2.6+ required, unreleased): +#### Chunking support (version 2.6+ required): influxdb-java client now supports influxdb chunking. The following example uses a chunkSize of 20 and invokes the specified Consumer (e.g. System.out.println) for each received QueryResult ```java @@ -146,6 +146,74 @@ influxDB.query(query, 20, queryResult -> System.out.println(queryResult)); ``` +#### QueryResult mapper to POJO (version 2.7+ required, unreleased): + +An alternative way to handle the QueryResult object is now available. +Supposing that you have a measurement _CPU_: +``` +> INSERT cpu,host=serverA,region=us_west idle=0.64,happydevop=false,uptimesecs=123456789i +> +> select * from cpu +name: cpu +time happydevop host idle region uptimesecs +---- ---------- ---- ---- ------ ---------- +2017-06-20T15:32:46.202829088Z false serverA 0.64 us_west 123456789 +``` +And the following tag keys: +``` +> show tag keys from cpu +name: cpu +tagKey +------ +host +region +``` + +1. Create a POJO to represent your measurement. For example: +```Java +public class Cpu { + private Instant time; + private String hostname; + private String region; + private Double idle; + private Boolean happydevop; + private Long uptimeSecs; + // getters (and setters if you need) +} +``` +2. Add @Measurement and @Column annotations: +```Java +@Measurement(name = "cpu") +public class Cpu { + @Column(name = "time") + private Instant time; + @Column(name = "host", tag = true) + private String hostname; + @Column(name = "region", tag = true) + private String region; + @Column(name = "idle") + private Double idle; + @Column(name = "happydevop") + private Boolean happydevop; + @Column(name = "uptimesecs") + private Long uptimeSecs; + // getters (and setters if you need) +} +``` +3. Call _InfluxDBResultMapper.toPOJO(...)_ to map the QueryResult to your POJO: +``` +InfluxDB influxDB = InfluxDBFactory.connect("http://localhost:8086", "root", "root"); +String dbName = "myTimeseries"; +QueryResult queryResult = influxDB.query(new Query("SELECT * FROM cpu", dbName)); + +InfluxResultMapper resultMapper = new InfluxResultMapper(); // thread-safe - can be reused +List cpuList = resultMapper.toPOJO(queryResult, Cpu.class); +``` +**QueryResult mapper limitations** +- If your InfluxDB query contains multiple SELECT clauses, you will have to call InfluxResultMapper#toPOJO() multiple times to map every measurement returned by QueryResult to the respective POJO; +- If your InfluxDB query contains multiple SELECT clauses **for the same measurement**, InfluxResultMapper will process all results because there is no way to distinguish which one should be mapped to your POJO. It may result in an invalid collection being returned; + + ### Other Usages: For additional usage examples have a look at [InfluxDBTest.java](https://github.com/influxdb/influxdb-java/blob/master/src/test/java/org/influxdb/InfluxDBTest.java "InfluxDBTest.java") diff --git a/src/main/java/org/influxdb/InfluxDBMapperException.java b/src/main/java/org/influxdb/InfluxDBMapperException.java new file mode 100644 index 000000000..a79dd9c7f --- /dev/null +++ b/src/main/java/org/influxdb/InfluxDBMapperException.java @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * @author fmachado + */ +public class InfluxDBMapperException extends RuntimeException { + + private static final long serialVersionUID = -7328402653918756407L; + + public InfluxDBMapperException(final String message, final Throwable cause) { + super(message, cause); + } + + public InfluxDBMapperException(final String message) { + super(message); + } + + public InfluxDBMapperException(final Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/influxdb/annotation/Column.java b/src/main/java/org/influxdb/annotation/Column.java new file mode 100644 index 000000000..cde2fbe50 --- /dev/null +++ b/src/main/java/org/influxdb/annotation/Column.java @@ -0,0 +1,38 @@ +/* + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author fmachado + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Column { + + String name(); + + boolean tag() default false; +} diff --git a/src/main/java/org/influxdb/annotation/Measurement.java b/src/main/java/org/influxdb/annotation/Measurement.java new file mode 100644 index 000000000..8310f0f98 --- /dev/null +++ b/src/main/java/org/influxdb/annotation/Measurement.java @@ -0,0 +1,39 @@ +/* + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * @author fmachado + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Measurement { + + String name(); + + TimeUnit timeUnit() default TimeUnit.MILLISECONDS; +} diff --git a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java new file mode 100644 index 000000000..beb07824d --- /dev/null +++ b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java @@ -0,0 +1,273 @@ +/* + * 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.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 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 = 6; + private static final boolean ADD_DECIMAL_POINT = true; + + /** + * When a query is executed without {@link TimeUnit}, InfluxDB returns the time + * column as an ISO8601 date. + */ + private static final DateTimeFormatter ISO8601_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .appendFraction(ChronoField.MICRO_OF_SECOND, FRACTION_MIN_WIDTH, FRACTION_MAX_WIDTH, ADD_DECIMAL_POINT) + .appendPattern("X") + .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 + * @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 { + throwExceptionIfMissingAnnotation(clazz); + throwExceptionIfResultWithError(queryResult); + cacheMeasurementClass(clazz); + + List result = new LinkedList(); + String measurementName = getMeasurementName(clazz); + queryResult.getResults().stream() + .forEach(singleResult -> { + singleResult.getSeries().stream() + .filter(series -> series.getName().equals(measurementName)) + .forEachOrdered(series -> { + parseSeriesAs(series, clazz, result); + }); + }); + + 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()); + } + }); + } + + 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; + } + + for (Field field : clazz.getDeclaredFields()) { + Column colAnnotation = field.getAnnotation(Column.class); + if (colAnnotation != null) { + influxColumnAndFieldMap.put(colAnnotation.name(), field); + } + } + } + } + + String getMeasurementName(final Class clazz) { + return ((Measurement) clazz.getAnnotation(Measurement.class)).name(); + } + + List parseSeriesAs(final QueryResult.Series series, final Class clazz, final List result) { + int columnSize = series.getColumns().size(); + try { + T object = null; + for (List row : series.getValues()) { + for (int i = 0; i < columnSize; i++) { + String resultColumnName = series.getColumns().get(i); + Field correspondingField = CLASS_FIELD_CACHE.get(clazz.getName()).get(resultColumnName); + if (correspondingField != null) { + if (object == null) { + object = clazz.newInstance(); + } + setFieldValue(object, correspondingField, row.get(i)); + } + } + 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 + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + void setFieldValue(final T object, final Field field, final Object value) + throws IllegalArgumentException, IllegalAccessException { + if (value == null) { + return; + } + Class fieldType = field.getType(); + boolean oldAccessibleState = field.isAccessible(); + try { + field.setAccessible(true); + if (fieldValueModified(fieldType, field, object, value) + || 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)); + } finally { + field.setAccessible(oldAccessibleState); + } + } + + boolean fieldValueModified(final Class fieldType, final Field field, final T object, final Object value) + 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(ISO8601_FORMATTER.parse(String.valueOf(value))); + } else if (value instanceof Long) { + instant = Instant.ofEpochMilli((Long) value); + } else if (value instanceof Double) { + instant = Instant.ofEpochMilli(((Double) value).longValue()); + } 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; + } +} diff --git a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java new file mode 100644 index 000000000..af096e571 --- /dev/null +++ b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java @@ -0,0 +1,279 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import org.influxdb.InfluxDBMapperException; +import org.influxdb.annotation.Column; +import org.influxdb.annotation.Measurement; +import org.influxdb.dto.QueryResult; +import org.influxdb.impl.InfluxDBResultMapper; +import org.junit.Test; + +/** + * @author fmachado + */ +public class InfluxDBResultMapperTest { + + InfluxDBResultMapper mapper = new InfluxDBResultMapper(); + + @Test + public void testToPOJO_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... + assertEquals("there must be one entry in the result list", 1, myList.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testThrowExceptionIfMissingAnnotation() { + mapper.throwExceptionIfMissingAnnotation(String.class); + } + + @Test(expected = InfluxDBMapperException.class) + public void testThrowExceptionIfError_InfluxQueryResultHasError() { + QueryResult queryResult = new QueryResult(); + queryResult.setError("main queryresult error"); + + mapper.throwExceptionIfResultWithError(queryResult); + } + + @Test(expected = InfluxDBMapperException.class) + public void testThrowExceptionIfError_InfluxQueryResultSeriesHasError() { + QueryResult queryResult = new QueryResult(); + + QueryResult.Result seriesResult = new QueryResult.Result(); + seriesResult.setError("series error"); + + queryResult.setResults(Arrays.asList(seriesResult)); + + mapper.throwExceptionIfResultWithError(queryResult); + } + + @Test + public void testGetMeasurementName_testStateMeasurement() { + assertEquals("CustomMeasurement", mapper.getMeasurementName(MyCustomMeasurement.class)); + } + + @Test + public void testParseSeriesAs_testTwoValidSeries() { + // Given... + mapper.cacheMeasurementClass(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... + assertTrue("there must be two series in the result list", result.size() == 2); + + assertEquals("Field 'time' (1st series) is not valid", firstSeriesResult.get(0), result.get(0).time.toEpochMilli()); + assertEquals("Field 'uuid' (1st series) is not valid", firstSeriesResult.get(1), result.get(0).uuid); + + assertEquals("Field 'time' (2nd series) is not valid", secondSeriesResult.get(0), result.get(1).time.toEpochMilli()); + assertEquals("Field 'uuid' (2nd series) is not valid", secondSeriesResult.get(1), result.get(1).uuid); + } + + @Test + public void testParseSeriesAs_testNonNullAndValidValues() { + // Given... + mapper.cacheMeasurementClass(MyCustomMeasurement.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(); + String uuidAsString = UUID.randomUUID().toString(); + + // InfluxDB client returns any number as Double. + // See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987 + // for more information. + 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"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + series.setValues(Arrays.asList(seriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + MyCustomMeasurement myObject = result.get(0); + assertEquals("field 'time' does not match", now.longValue(), myObject.time.toEpochMilli()); + assertEquals("field 'uuid' does not match", uuidAsString, myObject.uuid); + + assertEquals("field 'doubleObject' does not match", asDouble(seriesResult.get(2)), myObject.doubleObject); + assertEquals("field 'longObject' does not match", new Long(asDouble(seriesResult.get(3)).longValue()), myObject.longObject); + assertEquals("field 'integerObject' does not match", new Integer(asDouble(seriesResult.get(4)).intValue()), myObject.integerObject); + + assertTrue("field 'doublePrimitive' does not match", + Double.compare(asDouble(seriesResult.get(5)).doubleValue(), myObject.doublePrimitive) == 0); + + assertTrue("field 'longPrimitive' does not match", + Long.compare(asDouble(seriesResult.get(6)).longValue(), myObject.longPrimitive) == 0); + + assertTrue("field 'integerPrimitive' does not match", + Integer.compare(asDouble(seriesResult.get(7)).intValue(), myObject.integerPrimitive) == 0); + + assertEquals("booleanObject 'time' does not match", + Boolean.valueOf(String.valueOf(seriesResult.get(8))), myObject.booleanObject); + + assertEquals("booleanPrimitive 'uuid' does not match", + Boolean.valueOf(String.valueOf(seriesResult.get(9))).booleanValue(), myObject.booleanPrimitive); + } + + Double asDouble(Object obj) { + return (Double) obj; + } + + @Test + public void testFieldValueModified_DateAsISO8601() { + // Given... + mapper.cacheMeasurementClass(MyCustomMeasurement.class); + + List columnList = Arrays.asList("time"); + List firstSeriesResult = Arrays.asList("2017-06-19T09:29:45.655123Z"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + series.setValues(Arrays.asList(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyCustomMeasurement.class, result); + + //Then... + assertTrue(result.size() == 1); + } + + @Test(expected = InfluxDBMapperException.class) + public void testUnsupportedField() { + // Given... + mapper.cacheMeasurementClass(MyPojoWithUnsupportedField.class); + + List columnList = Arrays.asList("bar"); + List firstSeriesResult = Arrays.asList("content representing a Date"); + + QueryResult.Series series = new QueryResult.Series(); + series.setColumns(columnList); + series.setValues(Arrays.asList(firstSeriesResult)); + + //When... + List result = new LinkedList<>(); + mapper.parseSeriesAs(series, MyPojoWithUnsupportedField.class, result); + } + + @Measurement(name = "CustomMeasurement") + static class MyCustomMeasurement { + + @Column(name = "time") + private Instant time; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "doubleObject") + private Double doubleObject; + + @Column(name = "longObject") + private Long longObject; + + @Column(name = "integerObject") + private Integer integerObject; + + @Column(name = "doublePrimitive") + private double doublePrimitive; + + @Column(name = "longPrimitive") + private long longPrimitive; + + @Column(name = "integerPrimitive") + private int integerPrimitive; + + @Column(name = "booleanObject") + private Boolean booleanObject; + + @Column(name = "booleanPrimitive") + private boolean booleanPrimitive; + + @SuppressWarnings("unused") + private String nonColumn1; + + @SuppressWarnings("unused") + private Random rnd; + + @Override + public String toString() { + return "MyCustomMeasurement [time=" + time + ", uuid=" + uuid + ", doubleObject=" + doubleObject + ", longObject=" + longObject + + ", integerObject=" + integerObject + ", doublePrimitive=" + doublePrimitive + ", longPrimitive=" + longPrimitive + + ", integerPrimitive=" + integerPrimitive + ", booleanObject=" + booleanObject + ", booleanPrimitive=" + booleanPrimitive + "]"; + } + } + + @Measurement(name = "foo") + static class MyPojoWithUnsupportedField { + + @Column(name = "bar") + private Date myDate; + } +} \ No newline at end of file