Skip to content

Commit dd67c7d

Browse files
authoredJun 23, 2017
Merge pull request #341 from fmachado/master
QueryResult to Object mapper added
2 parents 205fc1d + c79770e commit dd67c7d

File tree

6 files changed

+739
-1
lines changed

6 files changed

+739
-1
lines changed
 

‎README.md

+69-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public void write(final int udpPort, final Point point);
137137
note: make sure write content's total size should not > UDP protocol's limit(64K), or you should use http instead of udp.
138138

139139

140-
#### chunking support (version 2.6+ required, unreleased):
140+
#### Chunking support (version 2.6+ required):
141141

142142
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
143143
```java
@@ -146,6 +146,74 @@ influxDB.query(query, 20, queryResult -> System.out.println(queryResult));
146146
```
147147

148148

149+
#### QueryResult mapper to POJO (version 2.7+ required, unreleased):
150+
151+
An alternative way to handle the QueryResult object is now available.
152+
Supposing that you have a measurement _CPU_:
153+
```
154+
> INSERT cpu,host=serverA,region=us_west idle=0.64,happydevop=false,uptimesecs=123456789i
155+
>
156+
> select * from cpu
157+
name: cpu
158+
time happydevop host idle region uptimesecs
159+
---- ---------- ---- ---- ------ ----------
160+
2017-06-20T15:32:46.202829088Z false serverA 0.64 us_west 123456789
161+
```
162+
And the following tag keys:
163+
```
164+
> show tag keys from cpu
165+
name: cpu
166+
tagKey
167+
------
168+
host
169+
region
170+
```
171+
172+
1. Create a POJO to represent your measurement. For example:
173+
```Java
174+
public class Cpu {
175+
private Instant time;
176+
private String hostname;
177+
private String region;
178+
private Double idle;
179+
private Boolean happydevop;
180+
private Long uptimeSecs;
181+
// getters (and setters if you need)
182+
}
183+
```
184+
2. Add @Measurement and @Column annotations:
185+
```Java
186+
@Measurement(name = "cpu")
187+
public class Cpu {
188+
@Column(name = "time")
189+
private Instant time;
190+
@Column(name = "host", tag = true)
191+
private String hostname;
192+
@Column(name = "region", tag = true)
193+
private String region;
194+
@Column(name = "idle")
195+
private Double idle;
196+
@Column(name = "happydevop")
197+
private Boolean happydevop;
198+
@Column(name = "uptimesecs")
199+
private Long uptimeSecs;
200+
// getters (and setters if you need)
201+
}
202+
```
203+
3. Call _InfluxDBResultMapper.toPOJO(...)_ to map the QueryResult to your POJO:
204+
```
205+
InfluxDB influxDB = InfluxDBFactory.connect("http://localhost:8086", "root", "root");
206+
String dbName = "myTimeseries";
207+
QueryResult queryResult = influxDB.query(new Query("SELECT * FROM cpu", dbName));
208+
209+
InfluxResultMapper resultMapper = new InfluxResultMapper(); // thread-safe - can be reused
210+
List<Cpu> cpuList = resultMapper.toPOJO(queryResult, Cpu.class);
211+
```
212+
**QueryResult mapper limitations**
213+
- 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;
214+
- 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;
215+
216+
149217
### Other Usages:
150218
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")
151219

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 azeti Networks AG (<info@azeti.net>)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package org.influxdb;
22+
23+
/**
24+
* @author fmachado
25+
*/
26+
public class InfluxDBMapperException extends RuntimeException {
27+
28+
private static final long serialVersionUID = -7328402653918756407L;
29+
30+
public InfluxDBMapperException(final String message, final Throwable cause) {
31+
super(message, cause);
32+
}
33+
34+
public InfluxDBMapperException(final String message) {
35+
super(message);
36+
}
37+
38+
public InfluxDBMapperException(final Throwable cause) {
39+
super(cause);
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 azeti Networks AG (<info@azeti.net>)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package org.influxdb.annotation;
22+
23+
import java.lang.annotation.ElementType;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
28+
/**
29+
* @author fmachado
30+
*/
31+
@Retention(RetentionPolicy.RUNTIME)
32+
@Target(ElementType.FIELD)
33+
public @interface Column {
34+
35+
String name();
36+
37+
boolean tag() default false;
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 azeti Networks AG (<info@azeti.net>)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package org.influxdb.annotation;
22+
23+
import java.lang.annotation.ElementType;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
import java.util.concurrent.TimeUnit;
28+
29+
/**
30+
* @author fmachado
31+
*/
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target(ElementType.TYPE)
34+
public @interface Measurement {
35+
36+
String name();
37+
38+
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 azeti Networks AG (<info@azeti.net>)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package org.influxdb.impl;
22+
23+
import java.lang.reflect.Field;
24+
import java.time.Instant;
25+
import java.time.format.DateTimeFormatter;
26+
import java.time.format.DateTimeFormatterBuilder;
27+
import java.time.temporal.ChronoField;
28+
import java.util.LinkedList;
29+
import java.util.List;
30+
import java.util.concurrent.ConcurrentHashMap;
31+
import java.util.concurrent.ConcurrentMap;
32+
import java.util.concurrent.TimeUnit;
33+
34+
import org.influxdb.InfluxDBMapperException;
35+
import org.influxdb.annotation.Column;
36+
import org.influxdb.annotation.Measurement;
37+
import org.influxdb.dto.QueryResult;
38+
39+
/**
40+
* Main class responsible for mapping a QueryResult to POJO.
41+
*
42+
* @author fmachado
43+
*/
44+
public class InfluxDBResultMapper {
45+
46+
/**
47+
* Data structure used to cache classes used as measurements.
48+
*/
49+
private static final
50+
ConcurrentMap<String, ConcurrentMap<String, Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();
51+
52+
private static final int FRACTION_MIN_WIDTH = 0;
53+
private static final int FRACTION_MAX_WIDTH = 6;
54+
private static final boolean ADD_DECIMAL_POINT = true;
55+
56+
/**
57+
* When a query is executed without {@link TimeUnit}, InfluxDB returns the <tt>time</tt>
58+
* column as an ISO8601 date.
59+
*/
60+
private static final DateTimeFormatter ISO8601_FORMATTER = new DateTimeFormatterBuilder()
61+
.appendPattern("yyyy-MM-dd'T'HH:mm:ss")
62+
.appendFraction(ChronoField.MICRO_OF_SECOND, FRACTION_MIN_WIDTH, FRACTION_MAX_WIDTH, ADD_DECIMAL_POINT)
63+
.appendPattern("X")
64+
.toFormatter();
65+
66+
/**
67+
* <p>
68+
* Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal
69+
* data structure and creating the respective object instances based on the Class passed as
70+
* parameter.
71+
* </p>
72+
*
73+
* @param queryResult the InfluxDB result object
74+
* @param clazz the Class that will be used to hold your measurement data
75+
* @return a {@link List} of objects from the same Class passed as parameter and sorted on the
76+
* same order as received from InfluxDB.
77+
* @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors,
78+
* <tt>clazz</tt> parameter is not annotated with &#64;Measurement or it was not
79+
* possible to define the values of your POJO (e.g. due to an unsupported field type).
80+
*/
81+
public <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz) throws InfluxDBMapperException {
82+
throwExceptionIfMissingAnnotation(clazz);
83+
throwExceptionIfResultWithError(queryResult);
84+
cacheMeasurementClass(clazz);
85+
86+
List<T> result = new LinkedList<T>();
87+
String measurementName = getMeasurementName(clazz);
88+
queryResult.getResults().stream()
89+
.forEach(singleResult -> {
90+
singleResult.getSeries().stream()
91+
.filter(series -> series.getName().equals(measurementName))
92+
.forEachOrdered(series -> {
93+
parseSeriesAs(series, clazz, result);
94+
});
95+
});
96+
97+
return result;
98+
}
99+
100+
void throwExceptionIfMissingAnnotation(final Class<?> clazz) {
101+
if (!clazz.isAnnotationPresent(Measurement.class)) {
102+
throw new IllegalArgumentException(
103+
"Class " + clazz.getName() + " is not annotated with @" + Measurement.class.getSimpleName());
104+
}
105+
}
106+
107+
void throwExceptionIfResultWithError(final QueryResult queryResult) {
108+
if (queryResult.getError() != null) {
109+
throw new InfluxDBMapperException("InfluxDB returned an error: " + queryResult.getError());
110+
}
111+
112+
queryResult.getResults().forEach(seriesResult -> {
113+
if (seriesResult.getError() != null) {
114+
throw new InfluxDBMapperException("InfluxDB returned an error with Series: " + seriesResult.getError());
115+
}
116+
});
117+
}
118+
119+
void cacheMeasurementClass(final Class<?>... classVarAgrs) {
120+
for (Class<?> clazz : classVarAgrs) {
121+
if (CLASS_FIELD_CACHE.containsKey(clazz.getName())) {
122+
continue;
123+
}
124+
ConcurrentMap<String, Field> initialMap = new ConcurrentHashMap<>();
125+
ConcurrentMap<String, Field> influxColumnAndFieldMap = CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), initialMap);
126+
if (influxColumnAndFieldMap == null) {
127+
influxColumnAndFieldMap = initialMap;
128+
}
129+
130+
for (Field field : clazz.getDeclaredFields()) {
131+
Column colAnnotation = field.getAnnotation(Column.class);
132+
if (colAnnotation != null) {
133+
influxColumnAndFieldMap.put(colAnnotation.name(), field);
134+
}
135+
}
136+
}
137+
}
138+
139+
String getMeasurementName(final Class<?> clazz) {
140+
return ((Measurement) clazz.getAnnotation(Measurement.class)).name();
141+
}
142+
143+
<T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result) {
144+
int columnSize = series.getColumns().size();
145+
try {
146+
T object = null;
147+
for (List<Object> row : series.getValues()) {
148+
for (int i = 0; i < columnSize; i++) {
149+
String resultColumnName = series.getColumns().get(i);
150+
Field correspondingField = CLASS_FIELD_CACHE.get(clazz.getName()).get(resultColumnName);
151+
if (correspondingField != null) {
152+
if (object == null) {
153+
object = clazz.newInstance();
154+
}
155+
setFieldValue(object, correspondingField, row.get(i));
156+
}
157+
}
158+
if (object != null) {
159+
result.add(object);
160+
object = null;
161+
}
162+
}
163+
} catch (InstantiationException | IllegalAccessException e) {
164+
throw new InfluxDBMapperException(e);
165+
}
166+
return result;
167+
}
168+
169+
/**
170+
* InfluxDB client returns any number as Double.
171+
* See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987
172+
* for more information.
173+
*
174+
* @param object
175+
* @param field
176+
* @param value
177+
* @throws IllegalArgumentException
178+
* @throws IllegalAccessException
179+
*/
180+
<T> void setFieldValue(final T object, final Field field, final Object value)
181+
throws IllegalArgumentException, IllegalAccessException {
182+
if (value == null) {
183+
return;
184+
}
185+
Class<?> fieldType = field.getType();
186+
boolean oldAccessibleState = field.isAccessible();
187+
try {
188+
field.setAccessible(true);
189+
if (fieldValueModified(fieldType, field, object, value)
190+
|| fieldValueForPrimitivesModified(fieldType, field, object, value)
191+
|| fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) {
192+
return;
193+
}
194+
String msg = "Class '%s' field '%s' is from an unsupported type '%s'.";
195+
throw new InfluxDBMapperException(
196+
String.format(msg, object.getClass().getName(), field.getName(), field.getType()));
197+
} catch (ClassCastException e) {
198+
String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. "
199+
+ "The correct type is '%s' (current field value: '%s').";
200+
throw new InfluxDBMapperException(
201+
String.format(msg, object.getClass().getName(), field.getName(), value.getClass().getName(), value));
202+
} finally {
203+
field.setAccessible(oldAccessibleState);
204+
}
205+
}
206+
207+
<T> boolean fieldValueModified(final Class<?> fieldType, final Field field, final T object, final Object value)
208+
throws IllegalArgumentException, IllegalAccessException {
209+
if (String.class.isAssignableFrom(fieldType)) {
210+
field.set(object, String.valueOf(value));
211+
return true;
212+
}
213+
if (Instant.class.isAssignableFrom(fieldType)) {
214+
Instant instant;
215+
if (value instanceof String) {
216+
instant = Instant.from(ISO8601_FORMATTER.parse(String.valueOf(value)));
217+
} else if (value instanceof Long) {
218+
instant = Instant.ofEpochMilli((Long) value);
219+
} else if (value instanceof Double) {
220+
instant = Instant.ofEpochMilli(((Double) value).longValue());
221+
} else {
222+
throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName());
223+
}
224+
field.set(object, instant);
225+
return true;
226+
}
227+
return false;
228+
}
229+
230+
<T> boolean fieldValueForPrimitivesModified(final Class<?> fieldType, final Field field, final T object,
231+
final Object value)
232+
throws IllegalArgumentException, IllegalAccessException {
233+
if (double.class.isAssignableFrom(fieldType)) {
234+
field.setDouble(object, ((Double) value).doubleValue());
235+
return true;
236+
}
237+
if (long.class.isAssignableFrom(fieldType)) {
238+
field.setLong(object, ((Double) value).longValue());
239+
return true;
240+
}
241+
if (int.class.isAssignableFrom(fieldType)) {
242+
field.setInt(object, ((Double) value).intValue());
243+
return true;
244+
}
245+
if (boolean.class.isAssignableFrom(fieldType)) {
246+
field.setBoolean(object, Boolean.valueOf(String.valueOf(value)).booleanValue());
247+
return true;
248+
}
249+
return false;
250+
}
251+
252+
<T> boolean fieldValueForPrimitiveWrappersModified(final Class<?> fieldType, final Field field, final T object,
253+
final Object value)
254+
throws IllegalArgumentException, IllegalAccessException {
255+
if (Double.class.isAssignableFrom(fieldType)) {
256+
field.set(object, value);
257+
return true;
258+
}
259+
if (Long.class.isAssignableFrom(fieldType)) {
260+
field.set(object, Long.valueOf(((Double) value).longValue()));
261+
return true;
262+
}
263+
if (Integer.class.isAssignableFrom(fieldType)) {
264+
field.set(object, Integer.valueOf(((Double) value).intValue()));
265+
return true;
266+
}
267+
if (Boolean.class.isAssignableFrom(fieldType)) {
268+
field.set(object, Boolean.valueOf(String.valueOf(value)));
269+
return true;
270+
}
271+
return false;
272+
}
273+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2017 azeti Networks AG (<info@azeti.net>)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package org.influxdb.impl;
22+
23+
import static org.junit.Assert.assertEquals;
24+
import static org.junit.Assert.assertTrue;
25+
26+
import java.time.Instant;
27+
import java.util.Arrays;
28+
import java.util.Date;
29+
import java.util.LinkedList;
30+
import java.util.List;
31+
import java.util.Random;
32+
import java.util.UUID;
33+
34+
import org.influxdb.InfluxDBMapperException;
35+
import org.influxdb.annotation.Column;
36+
import org.influxdb.annotation.Measurement;
37+
import org.influxdb.dto.QueryResult;
38+
import org.influxdb.impl.InfluxDBResultMapper;
39+
import org.junit.Test;
40+
41+
/**
42+
* @author fmachado
43+
*/
44+
public class InfluxDBResultMapperTest {
45+
46+
InfluxDBResultMapper mapper = new InfluxDBResultMapper();
47+
48+
@Test
49+
public void testToPOJO_HappyPath() {
50+
// Given...
51+
List<String> columnList = Arrays.asList("time", "uuid");
52+
List<Object> firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString());
53+
54+
QueryResult.Series series = new QueryResult.Series();
55+
series.setColumns(columnList);
56+
series.setName("CustomMeasurement");
57+
series.setValues(Arrays.asList(firstSeriesResult));
58+
59+
QueryResult.Result internalResult = new QueryResult.Result();
60+
internalResult.setSeries(Arrays.asList(series));
61+
62+
QueryResult queryResult = new QueryResult();
63+
queryResult.setResults(Arrays.asList(internalResult));
64+
65+
//When...
66+
List<MyCustomMeasurement> myList = mapper.toPOJO(queryResult, MyCustomMeasurement.class);
67+
68+
// Then...
69+
assertEquals("there must be one entry in the result list", 1, myList.size());
70+
}
71+
72+
@Test(expected = IllegalArgumentException.class)
73+
public void testThrowExceptionIfMissingAnnotation() {
74+
mapper.throwExceptionIfMissingAnnotation(String.class);
75+
}
76+
77+
@Test(expected = InfluxDBMapperException.class)
78+
public void testThrowExceptionIfError_InfluxQueryResultHasError() {
79+
QueryResult queryResult = new QueryResult();
80+
queryResult.setError("main queryresult error");
81+
82+
mapper.throwExceptionIfResultWithError(queryResult);
83+
}
84+
85+
@Test(expected = InfluxDBMapperException.class)
86+
public void testThrowExceptionIfError_InfluxQueryResultSeriesHasError() {
87+
QueryResult queryResult = new QueryResult();
88+
89+
QueryResult.Result seriesResult = new QueryResult.Result();
90+
seriesResult.setError("series error");
91+
92+
queryResult.setResults(Arrays.asList(seriesResult));
93+
94+
mapper.throwExceptionIfResultWithError(queryResult);
95+
}
96+
97+
@Test
98+
public void testGetMeasurementName_testStateMeasurement() {
99+
assertEquals("CustomMeasurement", mapper.getMeasurementName(MyCustomMeasurement.class));
100+
}
101+
102+
@Test
103+
public void testParseSeriesAs_testTwoValidSeries() {
104+
// Given...
105+
mapper.cacheMeasurementClass(MyCustomMeasurement.class);
106+
107+
List<String> columnList = Arrays.asList("time", "uuid");
108+
109+
List<Object> firstSeriesResult = Arrays.asList(Instant.now().toEpochMilli(), UUID.randomUUID().toString());
110+
List<Object> secondSeriesResult = Arrays.asList(Instant.now().plusSeconds(1).toEpochMilli(), UUID.randomUUID().toString());
111+
112+
QueryResult.Series series = new QueryResult.Series();
113+
series.setColumns(columnList);
114+
series.setValues(Arrays.asList(firstSeriesResult, secondSeriesResult));
115+
116+
//When...
117+
List<MyCustomMeasurement> result = new LinkedList<>();
118+
mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
119+
120+
//Then...
121+
assertTrue("there must be two series in the result list", result.size() == 2);
122+
123+
assertEquals("Field 'time' (1st series) is not valid", firstSeriesResult.get(0), result.get(0).time.toEpochMilli());
124+
assertEquals("Field 'uuid' (1st series) is not valid", firstSeriesResult.get(1), result.get(0).uuid);
125+
126+
assertEquals("Field 'time' (2nd series) is not valid", secondSeriesResult.get(0), result.get(1).time.toEpochMilli());
127+
assertEquals("Field 'uuid' (2nd series) is not valid", secondSeriesResult.get(1), result.get(1).uuid);
128+
}
129+
130+
@Test
131+
public void testParseSeriesAs_testNonNullAndValidValues() {
132+
// Given...
133+
mapper.cacheMeasurementClass(MyCustomMeasurement.class);
134+
135+
List<String> columnList = Arrays.asList("time", "uuid",
136+
"doubleObject", "longObject", "integerObject",
137+
"doublePrimitive", "longPrimitive", "integerPrimitive",
138+
"booleanObject", "booleanPrimitive");
139+
140+
// InfluxDB client returns the time representation as Double.
141+
Double now = Long.valueOf(System.currentTimeMillis()).doubleValue();
142+
String uuidAsString = UUID.randomUUID().toString();
143+
144+
// InfluxDB client returns any number as Double.
145+
// See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987
146+
// for more information.
147+
List<Object> seriesResult = Arrays.asList(now, uuidAsString,
148+
new Double("1.01"), new Double("2"), new Double("3"),
149+
new Double("1.01"), new Double("4"), new Double("5"),
150+
"false", "true");
151+
152+
QueryResult.Series series = new QueryResult.Series();
153+
series.setColumns(columnList);
154+
series.setValues(Arrays.asList(seriesResult));
155+
156+
//When...
157+
List<MyCustomMeasurement> result = new LinkedList<>();
158+
mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
159+
160+
//Then...
161+
MyCustomMeasurement myObject = result.get(0);
162+
assertEquals("field 'time' does not match", now.longValue(), myObject.time.toEpochMilli());
163+
assertEquals("field 'uuid' does not match", uuidAsString, myObject.uuid);
164+
165+
assertEquals("field 'doubleObject' does not match", asDouble(seriesResult.get(2)), myObject.doubleObject);
166+
assertEquals("field 'longObject' does not match", new Long(asDouble(seriesResult.get(3)).longValue()), myObject.longObject);
167+
assertEquals("field 'integerObject' does not match", new Integer(asDouble(seriesResult.get(4)).intValue()), myObject.integerObject);
168+
169+
assertTrue("field 'doublePrimitive' does not match",
170+
Double.compare(asDouble(seriesResult.get(5)).doubleValue(), myObject.doublePrimitive) == 0);
171+
172+
assertTrue("field 'longPrimitive' does not match",
173+
Long.compare(asDouble(seriesResult.get(6)).longValue(), myObject.longPrimitive) == 0);
174+
175+
assertTrue("field 'integerPrimitive' does not match",
176+
Integer.compare(asDouble(seriesResult.get(7)).intValue(), myObject.integerPrimitive) == 0);
177+
178+
assertEquals("booleanObject 'time' does not match",
179+
Boolean.valueOf(String.valueOf(seriesResult.get(8))), myObject.booleanObject);
180+
181+
assertEquals("booleanPrimitive 'uuid' does not match",
182+
Boolean.valueOf(String.valueOf(seriesResult.get(9))).booleanValue(), myObject.booleanPrimitive);
183+
}
184+
185+
Double asDouble(Object obj) {
186+
return (Double) obj;
187+
}
188+
189+
@Test
190+
public void testFieldValueModified_DateAsISO8601() {
191+
// Given...
192+
mapper.cacheMeasurementClass(MyCustomMeasurement.class);
193+
194+
List<String> columnList = Arrays.asList("time");
195+
List<Object> firstSeriesResult = Arrays.asList("2017-06-19T09:29:45.655123Z");
196+
197+
QueryResult.Series series = new QueryResult.Series();
198+
series.setColumns(columnList);
199+
series.setValues(Arrays.asList(firstSeriesResult));
200+
201+
//When...
202+
List<MyCustomMeasurement> result = new LinkedList<>();
203+
mapper.parseSeriesAs(series, MyCustomMeasurement.class, result);
204+
205+
//Then...
206+
assertTrue(result.size() == 1);
207+
}
208+
209+
@Test(expected = InfluxDBMapperException.class)
210+
public void testUnsupportedField() {
211+
// Given...
212+
mapper.cacheMeasurementClass(MyPojoWithUnsupportedField.class);
213+
214+
List<String> columnList = Arrays.asList("bar");
215+
List<Object> firstSeriesResult = Arrays.asList("content representing a Date");
216+
217+
QueryResult.Series series = new QueryResult.Series();
218+
series.setColumns(columnList);
219+
series.setValues(Arrays.asList(firstSeriesResult));
220+
221+
//When...
222+
List<MyPojoWithUnsupportedField> result = new LinkedList<>();
223+
mapper.parseSeriesAs(series, MyPojoWithUnsupportedField.class, result);
224+
}
225+
226+
@Measurement(name = "CustomMeasurement")
227+
static class MyCustomMeasurement {
228+
229+
@Column(name = "time")
230+
private Instant time;
231+
232+
@Column(name = "uuid")
233+
private String uuid;
234+
235+
@Column(name = "doubleObject")
236+
private Double doubleObject;
237+
238+
@Column(name = "longObject")
239+
private Long longObject;
240+
241+
@Column(name = "integerObject")
242+
private Integer integerObject;
243+
244+
@Column(name = "doublePrimitive")
245+
private double doublePrimitive;
246+
247+
@Column(name = "longPrimitive")
248+
private long longPrimitive;
249+
250+
@Column(name = "integerPrimitive")
251+
private int integerPrimitive;
252+
253+
@Column(name = "booleanObject")
254+
private Boolean booleanObject;
255+
256+
@Column(name = "booleanPrimitive")
257+
private boolean booleanPrimitive;
258+
259+
@SuppressWarnings("unused")
260+
private String nonColumn1;
261+
262+
@SuppressWarnings("unused")
263+
private Random rnd;
264+
265+
@Override
266+
public String toString() {
267+
return "MyCustomMeasurement [time=" + time + ", uuid=" + uuid + ", doubleObject=" + doubleObject + ", longObject=" + longObject
268+
+ ", integerObject=" + integerObject + ", doublePrimitive=" + doublePrimitive + ", longPrimitive=" + longPrimitive
269+
+ ", integerPrimitive=" + integerPrimitive + ", booleanObject=" + booleanObject + ", booleanPrimitive=" + booleanPrimitive + "]";
270+
}
271+
}
272+
273+
@Measurement(name = "foo")
274+
static class MyPojoWithUnsupportedField {
275+
276+
@Column(name = "bar")
277+
private Date myDate;
278+
}
279+
}

0 commit comments

Comments
 (0)
Please sign in to comment.