Skip to content

Commit 4b4dc44

Browse files
authored
Support generic POJO super classes (#980)
* Support generic POJO super classes * Add generic POJO super classes to MANUAL.md * Clean up TypeMapper
1 parent f324d22 commit 4b4dc44

File tree

6 files changed

+280
-108
lines changed

6 files changed

+280
-108
lines changed

MANUAL.md

+19
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,25 @@ influxDB.write(dbName, rpName, point);
402402

403403
An alternative way to create InfluxDB queries is available. By using the [QueryBuilder](QUERY_BUILDER.md) you can create queries using java instead of providing the influxdb queries as strings.
404404

405+
#### Generic POJO super classes
406+
407+
POJO classes can have generic super classes, for cases where multiple measurements have a similar structure, and differ by type(s), as in:
408+
409+
```java
410+
public class SuperMeasurement<T> {
411+
@Column
412+
@TimeColumn
413+
private Instant time;
414+
@Column
415+
T value;
416+
// Other common columns and tags
417+
}
418+
419+
public class SubMeasurement extends SuperMeasurement<String> {
420+
// Any specific columns and tags
421+
}
422+
```
423+
405424
### InfluxDBMapper
406425

407426
In case you want to save and load data using models you can use the [InfluxDBMapper](INFLUXDB_MAPPER.md).

src/main/java/org/influxdb/dto/Point.java

+22-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
import org.influxdb.annotation.Measurement;
88
import org.influxdb.annotation.TimeColumn;
99
import org.influxdb.impl.Preconditions;
10+
import org.influxdb.impl.TypeMapper;
1011

1112
import java.lang.annotation.Annotation;
1213
import java.lang.reflect.Field;
1314
import java.lang.reflect.Modifier;
15+
import java.lang.reflect.ParameterizedType;
16+
import java.lang.reflect.Type;
1417
import java.math.BigDecimal;
1518
import java.math.BigInteger;
1619
import java.math.RoundingMode;
@@ -286,6 +289,8 @@ public Builder addFieldsFromPOJO(final Object pojo) {
286289

287290
while (clazz != null) {
288291

292+
TypeMapper typeMapper = TypeMapper.empty();
293+
while (clazz != null) {
289294
for (Field field : clazz.getDeclaredFields()) {
290295

291296
Column column = field.getAnnotation(Column.class);
@@ -304,10 +309,20 @@ public Builder addFieldsFromPOJO(final Object pojo) {
304309
fieldName = field.getName();
305310
}
306311

307-
addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName);
312+
addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName, typeMapper);
313+
}
314+
315+
Class<?> superclass = clazz.getSuperclass();
316+
Type genericSuperclass = clazz.getGenericSuperclass();
317+
if (genericSuperclass instanceof ParameterizedType) {
318+
typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass);
319+
} else {
320+
typeMapper = TypeMapper.empty();
308321
}
309-
clazz = clazz.getSuperclass();
322+
323+
clazz = superclass;
310324
}
325+
}
311326

312327
if (this.fields.isEmpty()) {
313328
throw new BuilderException("Class " + pojo.getClass().getName()
@@ -318,13 +333,14 @@ public Builder addFieldsFromPOJO(final Object pojo) {
318333
}
319334

320335
private void addFieldByAttribute(final Object pojo, final Field field, final boolean tag,
321-
final String fieldName) {
336+
final String fieldName, final TypeMapper typeMapper) {
322337
try {
323338
Object fieldValue = field.get(pojo);
324339

325340
TimeColumn tc = field.getAnnotation(TimeColumn.class);
341+
Class<?> fieldType = (Class<?>) typeMapper.resolve(field.getGenericType());
326342
if (tc != null) {
327-
if (Instant.class.isAssignableFrom(field.getType())) {
343+
if (Instant.class.isAssignableFrom(fieldType)) {
328344
Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> {
329345
TimeUnit timeUnit = tc.timeUnit();
330346
if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) {
@@ -341,7 +357,7 @@ private void addFieldByAttribute(final Object pojo, final Field field, final boo
341357
}
342358

343359
throw new InfluxDBMapperException(
344-
"Unsupported type " + field.getType() + " for time: should be of Instant type");
360+
"Unsupported type " + fieldType + " for time: should be of Instant type");
345361
}
346362

347363
if (tag) {
@@ -350,7 +366,7 @@ private void addFieldByAttribute(final Object pojo, final Field field, final boo
350366
}
351367
} else {
352368
if (fieldValue != null) {
353-
setField(field.getType(), fieldName, fieldValue);
369+
setField(fieldType, fieldName, fieldValue);
354370
}
355371
}
356372

src/main/java/org/influxdb/impl/InfluxDBResultMapper.java

+83-102
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
import java.lang.reflect.Field;
3030
import java.lang.reflect.Modifier;
31+
import java.lang.reflect.ParameterizedType;
32+
import java.lang.reflect.Type;
3133
import java.time.Instant;
3234
import java.time.format.DateTimeFormatter;
3335
import java.time.format.DateTimeFormatterBuilder;
@@ -50,8 +52,12 @@ public class InfluxDBResultMapper {
5052
/**
5153
* Data structure used to cache classes used as measurements.
5254
*/
55+
private static class ClassInfo {
56+
ConcurrentMap<String, Field> fieldMap;
57+
ConcurrentMap<Field, TypeMapper> typeMappers;
58+
}
5359
private static final
54-
ConcurrentMap<String, ConcurrentMap<String, Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();
60+
ConcurrentMap<String, ClassInfo> CLASS_INFO_CACHE = new ConcurrentHashMap<>();
5561

5662
private static final int FRACTION_MIN_WIDTH = 0;
5763
private static final int FRACTION_MAX_WIDTH = 9;
@@ -204,21 +210,19 @@ void throwExceptionIfResultWithError(final QueryResult queryResult) {
204210
});
205211
}
206212

207-
ConcurrentMap<String, Field> getColNameAndFieldMap(final Class<?> clazz) {
208-
return CLASS_FIELD_CACHE.get(clazz.getName());
209-
}
210-
211213
void cacheMeasurementClass(final Class<?>... classVarAgrs) {
212214
for (Class<?> clazz : classVarAgrs) {
213-
if (CLASS_FIELD_CACHE.containsKey(clazz.getName())) {
215+
if (CLASS_INFO_CACHE.containsKey(clazz.getName())) {
214216
continue;
215217
}
216-
ConcurrentMap<String, Field> influxColumnAndFieldMap = new ConcurrentHashMap<>();
218+
ConcurrentMap<String, Field> fieldMap = new ConcurrentHashMap<>();
219+
ConcurrentMap<Field, TypeMapper> typeMappers = new ConcurrentHashMap<>();
217220

218221
Measurement measurement = clazz.getAnnotation(Measurement.class);
219222
boolean allFields = measurement != null && measurement.allFields();
220223

221224
Class<?> c = clazz;
225+
TypeMapper typeMapper = TypeMapper.empty();
222226
while (c != null) {
223227
for (Field field : c.getDeclaredFields()) {
224228
Column colAnnotation = field.getAnnotation(Column.class);
@@ -227,11 +231,25 @@ void cacheMeasurementClass(final Class<?>... classVarAgrs) {
227231
continue;
228232
}
229233

230-
influxColumnAndFieldMap.put(getFieldName(field, colAnnotation), field);
234+
fieldMap.put(getFieldName(field, colAnnotation), field);
235+
typeMappers.put(field, typeMapper);
231236
}
232-
c = c.getSuperclass();
237+
238+
Class<?> superclass = c.getSuperclass();
239+
Type genericSuperclass = c.getGenericSuperclass();
240+
if (genericSuperclass instanceof ParameterizedType) {
241+
typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass);
242+
} else {
243+
typeMapper = TypeMapper.empty();
244+
}
245+
246+
c = superclass;
233247
}
234-
CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), influxColumnAndFieldMap);
248+
249+
ClassInfo classInfo = new ClassInfo();
250+
classInfo.fieldMap = fieldMap;
251+
classInfo.typeMappers = typeMappers;
252+
CLASS_INFO_CACHE.putIfAbsent(clazz.getName(), classInfo);
235253
}
236254
}
237255

@@ -255,28 +273,26 @@ String getRetentionPolicy(final Class<?> clazz) {
255273
return ((Measurement) clazz.getAnnotation(Measurement.class)).retentionPolicy();
256274
}
257275

258-
TimeUnit getTimeUnit(final Class<?> clazz) {
259-
return ((Measurement) clazz.getAnnotation(Measurement.class)).timeUnit();
260-
}
261-
262276
<T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result) {
263277
return parseSeriesAs(series, clazz, result, TimeUnit.MILLISECONDS);
264278
}
265279

266280
<T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result,
267281
final TimeUnit precision) {
268282
int columnSize = series.getColumns().size();
269-
ConcurrentMap<String, Field> colNameAndFieldMap = CLASS_FIELD_CACHE.get(clazz.getName());
283+
284+
ClassInfo classInfo = CLASS_INFO_CACHE.get(clazz.getName());
270285
try {
271286
T object = null;
272287
for (List<Object> row : series.getValues()) {
273288
for (int i = 0; i < columnSize; i++) {
274-
Field correspondingField = colNameAndFieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/);
289+
Field correspondingField = classInfo.fieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/);
275290
if (correspondingField != null) {
276291
if (object == null) {
277292
object = clazz.newInstance();
278293
}
279-
setFieldValue(object, correspondingField, row.get(i), precision);
294+
setFieldValue(object, correspondingField, row.get(i), precision,
295+
classInfo.typeMappers.get(correspondingField));
280296
}
281297
}
282298
// When the "GROUP BY" clause is used, "tags" are returned as Map<String,String> and
@@ -285,10 +301,11 @@ <T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz,
285301
// "tag" values are always String.
286302
if (series.getTags() != null && !series.getTags().isEmpty()) {
287303
for (Entry<String, String> entry : series.getTags().entrySet()) {
288-
Field correspondingField = colNameAndFieldMap.get(entry.getKey()/*InfluxDB columnName*/);
304+
Field correspondingField = classInfo.fieldMap.get(entry.getKey()/*InfluxDB columnName*/);
289305
if (correspondingField != null) {
290306
// I don't think it is possible to reach here without a valid "object"
291-
setFieldValue(object, correspondingField, entry.getValue(), precision);
307+
setFieldValue(object, correspondingField, entry.getValue(), precision,
308+
classInfo.typeMappers.get(correspondingField));
292309
}
293310
}
294311
}
@@ -309,104 +326,68 @@ <T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz,
309326
* for more information.
310327
*
311328
*/
312-
private static <T> void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision)
329+
private static <T> void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision,
330+
final TypeMapper typeMapper)
313331
throws IllegalArgumentException, IllegalAccessException {
314332
if (value == null) {
315333
return;
316334
}
317-
Class<?> fieldType = field.getType();
335+
Type fieldType = typeMapper.resolve(field.getGenericType());
336+
if (!field.isAccessible()) {
337+
field.setAccessible(true);
338+
}
339+
field.set(object, adaptValue((Class<?>) fieldType, value, precision, field.getName(), object.getClass().getName()));
340+
}
341+
342+
private static Object adaptValue(final Class<?> fieldType, final Object value, final TimeUnit precision,
343+
final String fieldName, final String className) {
318344
try {
319-
if (!field.isAccessible()) {
320-
field.setAccessible(true);
345+
if (String.class.isAssignableFrom(fieldType)) {
346+
return String.valueOf(value);
347+
}
348+
if (Instant.class.isAssignableFrom(fieldType)) {
349+
if (value instanceof String) {
350+
return Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value)));
351+
}
352+
if (value instanceof Long) {
353+
return Instant.ofEpochMilli(toMillis((long) value, precision));
354+
}
355+
if (value instanceof Double) {
356+
return Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision));
357+
}
358+
if (value instanceof Integer) {
359+
return Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision));
360+
}
361+
throw new InfluxDBMapperException("Unsupported type " + fieldType + " for field " + fieldName);
321362
}
322-
if (fieldValueModified(fieldType, field, object, value, precision)
323-
|| fieldValueForPrimitivesModified(fieldType, field, object, value)
324-
|| fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) {
325-
return;
363+
if (Double.class.isAssignableFrom(fieldType) || double.class.isAssignableFrom(fieldType)) {
364+
return value;
365+
}
366+
if (Long.class.isAssignableFrom(fieldType) || long.class.isAssignableFrom(fieldType)) {
367+
return ((Double) value).longValue();
368+
}
369+
if (Integer.class.isAssignableFrom(fieldType) || int.class.isAssignableFrom(fieldType)) {
370+
return ((Double) value).intValue();
371+
}
372+
if (Boolean.class.isAssignableFrom(fieldType) || boolean.class.isAssignableFrom(fieldType)) {
373+
return Boolean.valueOf(String.valueOf(value));
374+
}
375+
if (Enum.class.isAssignableFrom(fieldType)) {
376+
//noinspection unchecked
377+
return Enum.valueOf((Class<Enum>) fieldType, String.valueOf(value));
326378
}
327-
String msg = "Class '%s' field '%s' is from an unsupported type '%s'.";
328-
throw new InfluxDBMapperException(
329-
String.format(msg, object.getClass().getName(), field.getName(), field.getType()));
330379
} catch (ClassCastException e) {
331380
String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. "
332381
+ "The correct type is '%s' (current field value: '%s').";
333382
throw new InfluxDBMapperException(
334-
String.format(msg, object.getClass().getName(), field.getName(), value.getClass().getName(), value));
335-
}
336-
}
337-
338-
static <T> boolean fieldValueModified(final Class<?> fieldType, final Field field, final T object, final Object value,
339-
final TimeUnit precision)
340-
throws IllegalArgumentException, IllegalAccessException {
341-
if (String.class.isAssignableFrom(fieldType)) {
342-
field.set(object, String.valueOf(value));
343-
return true;
344-
}
345-
if (Instant.class.isAssignableFrom(fieldType)) {
346-
Instant instant;
347-
if (value instanceof String) {
348-
instant = Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value)));
349-
} else if (value instanceof Long) {
350-
instant = Instant.ofEpochMilli(toMillis((long) value, precision));
351-
} else if (value instanceof Double) {
352-
instant = Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision));
353-
} else if (value instanceof Integer) {
354-
instant = Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision));
355-
} else {
356-
throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName());
357-
}
358-
field.set(object, instant);
359-
return true;
360-
}
361-
return false;
362-
}
363-
364-
static <T> boolean fieldValueForPrimitivesModified(final Class<?> fieldType, final Field field, final T object,
365-
final Object value)
366-
throws IllegalArgumentException, IllegalAccessException {
367-
if (double.class.isAssignableFrom(fieldType)) {
368-
field.setDouble(object, ((Double) value).doubleValue());
369-
return true;
370-
}
371-
if (long.class.isAssignableFrom(fieldType)) {
372-
field.setLong(object, ((Double) value).longValue());
373-
return true;
374-
}
375-
if (int.class.isAssignableFrom(fieldType)) {
376-
field.setInt(object, ((Double) value).intValue());
377-
return true;
383+
String.format(msg, className, fieldName, value.getClass().getName(), value));
378384
}
379-
if (boolean.class.isAssignableFrom(fieldType)) {
380-
field.setBoolean(object, Boolean.valueOf(String.valueOf(value)).booleanValue());
381-
return true;
382-
}
383-
return false;
384-
}
385385

386-
static <T> boolean fieldValueForPrimitiveWrappersModified(final Class<?> fieldType, final Field field, final T object,
387-
final Object value)
388-
throws IllegalArgumentException, IllegalAccessException {
389-
if (Double.class.isAssignableFrom(fieldType)) {
390-
field.set(object, value);
391-
return true;
392-
}
393-
if (Long.class.isAssignableFrom(fieldType)) {
394-
field.set(object, Long.valueOf(((Double) value).longValue()));
395-
return true;
396-
}
397-
if (Integer.class.isAssignableFrom(fieldType)) {
398-
field.set(object, Integer.valueOf(((Double) value).intValue()));
399-
return true;
400-
}
401-
if (Boolean.class.isAssignableFrom(fieldType)) {
402-
field.set(object, Boolean.valueOf(String.valueOf(value)));
403-
return true;
404-
}
405-
return false;
386+
throw new InfluxDBMapperException(
387+
String.format("Class '%s' field '%s' is from an unsupported type '%s'.", className, fieldName, fieldType));
406388
}
407389

408-
private static Long toMillis(final long value, final TimeUnit precision) {
409-
390+
private static long toMillis(final long value, final TimeUnit precision) {
410391
return TimeUnit.MILLISECONDS.convert(value, precision);
411392
}
412393
}

0 commit comments

Comments
 (0)