Repository: influxdata/influxdb-java
Pull request #980 (2023-12-05 20:14:29)

Support generic POJO super classes
Also, simplify type checking.

Should I add this to the manual?


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata) Report
Attention: `14 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`e93949f`)](https://app.codecov.io/gh/influxdata/influxdb-java/commit/e93949f9a359b48a0af9cbd378ced79044bd5caf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata) 60.98% compared to head [(`ee0268e`)](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata) 54.79%.

> :exclamation: Current head ee0268e differs from pull request most recent head 4893a71. Consider uploading reports for the commit 4893a71 to get more accurate results

| [Files](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata) | Patch % | Lines |
|---|---|---|
| [src/main/java/org/influxdb/impl/TypeMapper.java](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata#diff-c3JjL21haW4vamF2YS9vcmcvaW5mbHV4ZGIvaW1wbC9UeXBlTWFwcGVyLmphdmE=) | 57.14% | [7 Missing and 2 partials :warning: ](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata) |
| [...n/java/org/influxdb/impl/InfluxDBResultMapper.java](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata#diff-c3JjL21haW4vamF2YS9vcmcvaW5mbHV4ZGIvaW1wbC9JbmZsdXhEQlJlc3VsdE1hcHBlci5qYXZh) | 90.74% | [3 Missing and 2 partials :warning: ](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##             master     #980      +/-   ##
============================================
- Coverage     60.98%   54.79%   -6.19%     
+ Complexity      435      387      -48     
============================================
  Files            70       71       +1     
  Lines          2568     2588      +20     
  Branches        276      279       +3     
============================================
- Hits           1566     1418     -148     
- Misses          932     1100     +168     
  Partials         70       70              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/influxdata/influxdb-java/pull/980?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=influxdata).

majst01: Support for Generics is for sure worth mentioning in the manual.

Patch:
diff --git a/MANUAL.md b/MANUAL.md
index 18040ae..92b245b 100644
--- a/MANUAL.md
+++ b/MANUAL.md
@@ -402,6 +402,25 @@ influxDB.write(dbName, rpName, point);
 
 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.
 
+#### Generic POJO super classes
+
+POJO classes can have generic super classes, for cases where multiple measurements have a similar structure, and differ by type(s), as in:
+
+```java
+public class SuperMeasurement<T> {
+    @Column
+    @TimeColumn
+    private Instant time;
+    @Column
+    T value;
+    // Other common columns and tags
+}
+
+public class SubMeasurement extends SuperMeasurement<String> {
+    // Any specific columns and tags
+}
+```
+
 ### InfluxDBMapper
 
 In case you want to save and load data using models you can use the [InfluxDBMapper](INFLUXDB_MAPPER.md).
diff --git a/pom.xml b/pom.xml
index 1ee9a1a..beb3774 100644
--- a/pom.xml
+++ b/pom.xml
@@ -218,7 +218,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
-        <version>3.3.0</version>
+        <version>3.3.1</version>
         <dependencies>
           <dependency>
             <groupId>com.puppycrawl.tools</groupId>
diff --git a/src/main/java/org/influxdb/dto/Point.java b/src/main/java/org/influxdb/dto/Point.java
index 48fa764..277ce4c 100755
--- a/src/main/java/org/influxdb/dto/Point.java
+++ b/src/main/java/org/influxdb/dto/Point.java
@@ -7,10 +7,13 @@ import org.influxdb.annotation.Exclude;
 import org.influxdb.annotation.Measurement;
 import org.influxdb.annotation.TimeColumn;
 import org.influxdb.impl.Preconditions;
+import org.influxdb.impl.TypeMapper;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.RoundingMode;
@@ -286,6 +289,8 @@ public class Point {
 
       while (clazz != null) {
 
+      TypeMapper typeMapper = TypeMapper.empty();
+      while (clazz != null) {
         for (Field field : clazz.getDeclaredFields()) {
 
           Column column = field.getAnnotation(Column.class);
@@ -304,10 +309,20 @@ public class Point {
             fieldName = field.getName();
           }
 
-          addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName);
+          addFieldByAttribute(pojo, field, column != null && column.tag(), fieldName, typeMapper);
+        }
+
+        Class<?> superclass = clazz.getSuperclass();
+        Type genericSuperclass = clazz.getGenericSuperclass();
+        if (genericSuperclass instanceof ParameterizedType) {
+          typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass);
+        } else {
+          typeMapper = TypeMapper.empty();
         }
-        clazz = clazz.getSuperclass();
+
+        clazz = superclass;
       }
+    }
 
       if (this.fields.isEmpty()) {
         throw new BuilderException("Class " + pojo.getClass().getName()
@@ -318,13 +333,14 @@ public class Point {
     }
 
     private void addFieldByAttribute(final Object pojo, final Field field, final boolean tag,
-                                     final String fieldName) {
+                                     final String fieldName, final TypeMapper typeMapper) {
       try {
         Object fieldValue = field.get(pojo);
 
         TimeColumn tc = field.getAnnotation(TimeColumn.class);
+        Class<?> fieldType = (Class<?>) typeMapper.resolve(field.getGenericType());
         if (tc != null) {
-          if (Instant.class.isAssignableFrom(field.getType())) {
+          if (Instant.class.isAssignableFrom(fieldType)) {
             Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> {
               TimeUnit timeUnit = tc.timeUnit();
               if (timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) {
@@ -341,7 +357,7 @@ public class Point {
           }
 
           throw new InfluxDBMapperException(
-              "Unsupported type " + field.getType() + " for time: should be of Instant type");
+              "Unsupported type " + fieldType + " for time: should be of Instant type");
         }
 
         if (tag) {
@@ -350,7 +366,7 @@ public class Point {
           }
         } else {
           if (fieldValue != null) {
-            setField(field.getType(), fieldName, fieldValue);
+            setField(fieldType, fieldName, fieldValue);
           }
         }
 
diff --git a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java
index 15d4c28..5a4d3af 100644
--- a/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java
+++ b/src/main/java/org/influxdb/impl/InfluxDBResultMapper.java
@@ -28,6 +28,8 @@ import org.influxdb.dto.QueryResult;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.time.Instant;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
@@ -50,8 +52,12 @@ public class InfluxDBResultMapper {
   /**
    * Data structure used to cache classes used as measurements.
    */
+  private static class ClassInfo {
+    ConcurrentMap<String, Field> fieldMap;
+    ConcurrentMap<Field, TypeMapper> typeMappers;
+  }
   private static final
-    ConcurrentMap<String, ConcurrentMap<String, Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();
+    ConcurrentMap<String, ClassInfo> CLASS_INFO_CACHE = new ConcurrentHashMap<>();
 
   private static final int FRACTION_MIN_WIDTH = 0;
   private static final int FRACTION_MAX_WIDTH = 9;
@@ -204,21 +210,19 @@ public class InfluxDBResultMapper {
     });
   }
 
-  ConcurrentMap<String, Field> getColNameAndFieldMap(final Class<?> clazz) {
-    return CLASS_FIELD_CACHE.get(clazz.getName());
-  }
-
   void cacheMeasurementClass(final Class<?>... classVarAgrs) {
     for (Class<?> clazz : classVarAgrs) {
-      if (CLASS_FIELD_CACHE.containsKey(clazz.getName())) {
+      if (CLASS_INFO_CACHE.containsKey(clazz.getName())) {
         continue;
       }
-      ConcurrentMap<String, Field> influxColumnAndFieldMap = new ConcurrentHashMap<>();
+      ConcurrentMap<String, Field> fieldMap = new ConcurrentHashMap<>();
+      ConcurrentMap<Field, TypeMapper> typeMappers = new ConcurrentHashMap<>();
 
       Measurement measurement = clazz.getAnnotation(Measurement.class);
       boolean allFields = measurement != null && measurement.allFields();
 
       Class<?> c = clazz;
+      TypeMapper typeMapper = TypeMapper.empty();
       while (c != null) {
         for (Field field : c.getDeclaredFields()) {
           Column colAnnotation = field.getAnnotation(Column.class);
@@ -227,11 +231,25 @@ public class InfluxDBResultMapper {
             continue;
           }
 
-          influxColumnAndFieldMap.put(getFieldName(field, colAnnotation), field);
+          fieldMap.put(getFieldName(field, colAnnotation), field);
+          typeMappers.put(field, typeMapper);
         }
-        c = c.getSuperclass();
+
+        Class<?> superclass = c.getSuperclass();
+        Type genericSuperclass = c.getGenericSuperclass();
+        if (genericSuperclass instanceof ParameterizedType) {
+          typeMapper = TypeMapper.of((ParameterizedType) genericSuperclass, superclass);
+        } else {
+          typeMapper = TypeMapper.empty();
+        }
+
+        c = superclass;
       }
-      CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), influxColumnAndFieldMap);
+
+      ClassInfo classInfo = new ClassInfo();
+      classInfo.fieldMap = fieldMap;
+      classInfo.typeMappers = typeMappers;
+      CLASS_INFO_CACHE.putIfAbsent(clazz.getName(), classInfo);
     }
   }
 
@@ -255,10 +273,6 @@ public class InfluxDBResultMapper {
     return ((Measurement) clazz.getAnnotation(Measurement.class)).retentionPolicy();
   }
 
-  TimeUnit getTimeUnit(final Class<?> clazz) {
-    return ((Measurement) clazz.getAnnotation(Measurement.class)).timeUnit();
-  }
-
   <T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result) {
     return parseSeriesAs(series, clazz, result, TimeUnit.MILLISECONDS);
   }
@@ -266,17 +280,19 @@ public class InfluxDBResultMapper {
   <T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result,
                             final TimeUnit precision) {
     int columnSize = series.getColumns().size();
-    ConcurrentMap<String, Field> colNameAndFieldMap = CLASS_FIELD_CACHE.get(clazz.getName());
+
+    ClassInfo classInfo = CLASS_INFO_CACHE.get(clazz.getName());
     try {
       T object = null;
       for (List<Object> row : series.getValues()) {
         for (int i = 0; i < columnSize; i++) {
-          Field correspondingField = colNameAndFieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/);
+          Field correspondingField = classInfo.fieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/);
           if (correspondingField != null) {
             if (object == null) {
               object = clazz.newInstance();
             }
-            setFieldValue(object, correspondingField, row.get(i), precision);
+            setFieldValue(object, correspondingField, row.get(i), precision,
+                    classInfo.typeMappers.get(correspondingField));
           }
         }
         // When the "GROUP BY" clause is used, "tags" are returned as Map<String,String> and
@@ -285,10 +301,11 @@ public class InfluxDBResultMapper {
         // "tag" values are always String.
         if (series.getTags() != null && !series.getTags().isEmpty()) {
           for (Entry<String, String> entry : series.getTags().entrySet()) {
-            Field correspondingField = colNameAndFieldMap.get(entry.getKey()/*InfluxDB columnName*/);
+            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);
+              setFieldValue(object, correspondingField, entry.getValue(), precision,
+                      classInfo.typeMappers.get(correspondingField));
             }
           }
         }
@@ -309,104 +326,68 @@ public class InfluxDBResultMapper {
    * for more information.
    *
    */
-  private static <T> void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision)
+  private static <T> void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision,
+                                        final TypeMapper typeMapper)
     throws IllegalArgumentException, IllegalAccessException {
     if (value == null) {
       return;
     }
-    Class<?> fieldType = field.getType();
+    Type fieldType = typeMapper.resolve(field.getGenericType());
+    if (!field.isAccessible()) {
+      field.setAccessible(true);
+    }
+    field.set(object, adaptValue((Class<?>) fieldType, value, precision, field.getName(), object.getClass().getName()));
+  }
+
+  private static Object adaptValue(final Class<?> fieldType, final Object value, final TimeUnit precision,
+                                   final String fieldName, final String className) {
     try {
-      if (!field.isAccessible()) {
-        field.setAccessible(true);
+      if (String.class.isAssignableFrom(fieldType)) {
+        return String.valueOf(value);
+      }
+      if (Instant.class.isAssignableFrom(fieldType)) {
+        if (value instanceof String) {
+          return Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value)));
+        }
+        if (value instanceof Long) {
+          return Instant.ofEpochMilli(toMillis((long) value, precision));
+        }
+        if (value instanceof Double) {
+          return Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision));
+        }
+        if (value instanceof Integer) {
+          return Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision));
+        }
+        throw new InfluxDBMapperException("Unsupported type " + fieldType + " for field " + fieldName);
       }
-      if (fieldValueModified(fieldType, field, object, value, precision)
-        || fieldValueForPrimitivesModified(fieldType, field, object, value)
-        || fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) {
-        return;
+      if (Double.class.isAssignableFrom(fieldType) || double.class.isAssignableFrom(fieldType)) {
+        return value;
+      }
+      if (Long.class.isAssignableFrom(fieldType) || long.class.isAssignableFrom(fieldType)) {
+        return ((Double) value).longValue();
+      }
+      if (Integer.class.isAssignableFrom(fieldType) || int.class.isAssignableFrom(fieldType)) {
+        return ((Double) value).intValue();
+      }
+      if (Boolean.class.isAssignableFrom(fieldType) || boolean.class.isAssignableFrom(fieldType)) {
+        return Boolean.valueOf(String.valueOf(value));
+      }
+      if (Enum.class.isAssignableFrom(fieldType)) {
+        //noinspection unchecked
+        return Enum.valueOf((Class<Enum>) fieldType, String.valueOf(value));
       }
-      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));
-    }
-  }
-
-  static <T> boolean fieldValueModified(final Class<?> fieldType, final Field field, final T object, final Object value,
-                                        final TimeUnit precision)
-    throws IllegalArgumentException, IllegalAccessException {
-    if (String.class.isAssignableFrom(fieldType)) {
-      field.set(object, String.valueOf(value));
-      return true;
-    }
-    if (Instant.class.isAssignableFrom(fieldType)) {
-      Instant instant;
-      if (value instanceof String) {
-        instant = Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value)));
-      } else if (value instanceof Long) {
-        instant = Instant.ofEpochMilli(toMillis((long) value, precision));
-      } else if (value instanceof Double) {
-        instant = Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision));
-      } else if (value instanceof Integer) {
-        instant = Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision));
-      } else {
-        throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName());
-      }
-      field.set(object, instant);
-      return true;
-    }
-    return false;
-  }
-
-  static <T> 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;
+        String.format(msg, className, fieldName, value.getClass().getName(), value));
     }
-    if (boolean.class.isAssignableFrom(fieldType)) {
-      field.setBoolean(object, Boolean.valueOf(String.valueOf(value)).booleanValue());
-      return true;
-    }
-    return false;
-  }
 
-  static <T> 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;
+    throw new InfluxDBMapperException(
+           String.format("Class '%s' field '%s' is from an unsupported type '%s'.", className, fieldName, fieldType));
   }
 
-  private static Long toMillis(final long value, final TimeUnit precision) {
-
+  private static long toMillis(final long value, final TimeUnit precision) {
     return TimeUnit.MILLISECONDS.convert(value, precision);
   }
 }
diff --git a/src/main/java/org/influxdb/impl/TypeMapper.java b/src/main/java/org/influxdb/impl/TypeMapper.java
new file mode 100644
index 0000000..98f4ada
--- /dev/null
+++ b/src/main/java/org/influxdb/impl/TypeMapper.java
@@ -0,0 +1,70 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2017 azeti Networks AG (<info@azeti.net>)
+ *
+ * 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.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Resolves generic type variables to actual types, based on context.
+ *
+ * @author Eran Leshem
+ */
+@FunctionalInterface
+public interface TypeMapper {
+  TypeMapper EMPTY = typeVariable -> null;
+
+  static TypeMapper of(ParameterizedType type, Class<?> clazz) {
+    TypeVariable<? extends Class<?>>[] typeVariables = clazz.getTypeParameters();
+    Type[] types = type.getActualTypeArguments();
+    if (types.length != typeVariables.length) {
+      throw new IllegalStateException("Mismatched lengths for type variables and actual types");
+    }
+    Map<TypeVariable<?>, Type> typeMapping = new HashMap<>(typeVariables.length);
+    for (int i = 0; i < typeVariables.length; i++) {
+      typeMapping.put(typeVariables[i], types[i]);
+    }
+
+    return typeMapping::get;
+  }
+
+  static TypeMapper empty() {
+    return EMPTY;
+  }
+
+  default Type resolve(Type type) {
+    if (type instanceof TypeVariable) {
+      Type resolvedType = get((TypeVariable<?>) type);
+      if (resolvedType == null) {
+        throw new IllegalStateException("Could not resolve type " + type);
+      }
+
+      return resolvedType;
+    }
+
+    return type;
+  }
+
+  Type get(TypeVariable<?> typeVariable);
+}


Test patch:
diff --git a/src/test/java/org/influxdb/dto/PointTest.java b/src/test/java/org/influxdb/dto/PointTest.java
index 4121d40..9148c64 100755
--- a/src/test/java/org/influxdb/dto/PointTest.java
+++ b/src/test/java/org/influxdb/dto/PointTest.java
@@ -890,6 +890,22 @@ public class PointTest {
         Assert.assertEquals(expected, actual);
     }
 
+    @Test
+    public void testGenericInheritMeasurement() {
+        Point expected = Point.measurementByPOJO(MyGenericSubMeasurement.class)
+                .addField("superValue", "super")
+                .addField("subValue", "sub")
+                .build();
+        MyGenericSubMeasurement scm = new MyGenericSubMeasurement();
+        scm.subValue = "sub";
+        scm.superValue = "super";
+
+        Point actual = Point.measurementByPOJO(MyGenericSubMeasurement.class)
+                .addFieldsFromPOJO(scm)
+                .build();
+        Assert.assertEquals(expected, actual);
+    }
+
     static class PojoWithoutAnnotation {
 
         private String id;
@@ -1173,6 +1189,20 @@ public class PointTest {
         String subValue;
     }
 
+    @Measurement(name = "SuperMeasurement")
+    static class MyGenericSuperMeasurement<T> {
+
+      @Column(name = "superValue")
+      protected T superValue;
+    }
+
+    @Measurement(name = "SubMeasurement")
+    static class MyGenericSubMeasurement extends MyGenericSuperMeasurement<String> {
+
+      @Column(name = "subValue")
+      protected String subValue;
+    }
+
     @Measurement(name = "PojoNumberPrimitiveTypes")
     static class PojoNumberPrimitiveTypes
     {
diff --git a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java
index d93c4d3..9596782 100644
--- a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java
+++ b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java
@@ -449,6 +449,38 @@ public class InfluxDBResultMapperTest {
     Assertions.assertEquals(subValue, result.get(0).subValue);
   }
 
+	@Test
+ void testToPOJOGenericInheritance() {
+   // Given...
+   mapper.cacheMeasurementClass(MyGenericSubMeasurement.class);
+
+   String superValue = UUID.randomUUID().toString();
+   String subValue = "my sub value";
+   List<String> columnList = Arrays.asList("superValue", "subValue");
+
+   List<Object> firstSeriesResult = Arrays.asList(superValue, subValue);
+
+   QueryResult.Series series = new QueryResult.Series();
+   series.setName("MySeriesName");
+   series.setColumns(columnList);
+   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<MyGenericSubMeasurement> result =
+       mapper.toPOJO(queryResult, MyGenericSubMeasurement.class, "MySeriesName");
+
+   //Then...
+   Assertions.assertTrue(result.size() == 1);
+   Assertions.assertEquals(superValue, result.get(0).superValue);
+   Assertions.assertEquals(subValue, result.get(0).subValue);
+ }
+
   @Test
   public void testToPOJO_HasTimeColumn() {
 	  // Given...
@@ -638,6 +670,30 @@ public class InfluxDBResultMapperTest {
     }
   }
 
+	@Measurement(name = "SuperMeasurement")
+ static class MyGenericSuperMeasurement<T> {
+
+   @Column(name = "superValue")
+   protected T superValue;
+
+   @Override
+   public String toString() {
+     return "SuperMeasurement [superValue=" + superValue + "]";
+   }
+ }
+
+ @Measurement(name = "SubMeasurement")
+ static class MyGenericSubMeasurement extends MyGenericSuperMeasurement<String> {
+
+   @Column(name = "subValue")
+   protected String subValue;
+
+   @Override
+   public String toString() {
+     return "MySubMeasurement [subValue=" + subValue + ", superValue=" + superValue + "]";
+   }
+ }
+
   @Measurement(name = "foo")
 	static class MyPojoWithUnsupportedField {