Repository: influxdata/influxdb-java
Pull request #597 (2019-05-24 13:15:02)

pojo to timestamp field
i add a new annotation for timestamp so that we can set point time and precision attribute when use addFieldsFromPOJO method,and also can reslove UnableToParseException when use this method with a pojo class has field which type is Instant


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 065fead..dab7799 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 2.16
+
+### Fixes
+
+- Add new annotation called TimeColumn for timestamp field in POJO bean, this can set Point time and precision field correctly, also avoid UnableToParseException when flush Point to influx.
+
 ## 2.15 [2019-02-22]
 
 ### Fixes
diff --git a/README.md b/README.md
index f39ef8e..95f858a 100644
--- a/README.md
+++ b/README.md
@@ -231,11 +231,12 @@ public class Cpu {
 }
 ```
 
-2. Add @Measurement and @Column annotations:
+2. Add @Measurement,@TimeColumn and @Column annotations:
 
 ```Java
 @Measurement(name = "cpu")
 public class Cpu {
+    @TimeColumn
     @Column(name = "time")
     private Instant time;
     @Column(name = "host", tag = true)
@@ -270,7 +271,7 @@ Having the same POJO class Cpu
 ```java
 String dbName = "myTimeseries";
 String rpName = "aRetentionPolicy";
-// Cpu has annotations @Measurement and @Column
+// Cpu has annotations @Measurement,@TimeColumn and @Column
 Cpu cpu = new Cpu();
 // ... setting data
 
diff --git a/src/main/java/org/influxdb/annotation/TimeColumn.java b/src/main/java/org/influxdb/annotation/TimeColumn.java
new file mode 100644
index 0000000..94ed1d8
--- /dev/null
+++ b/src/main/java/org/influxdb/annotation/TimeColumn.java
@@ -0,0 +1,13 @@
+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;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface TimeColumn {
+    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
+}
diff --git a/src/main/java/org/influxdb/dto/Point.java b/src/main/java/org/influxdb/dto/Point.java
index cc70a22..b1385ac 100644
--- a/src/main/java/org/influxdb/dto/Point.java
+++ b/src/main/java/org/influxdb/dto/Point.java
@@ -5,15 +5,18 @@ import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.text.NumberFormat;
+import java.time.Instant;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.TreeMap;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import org.influxdb.BuilderException;
 import org.influxdb.annotation.Column;
 import org.influxdb.annotation.Measurement;
+import org.influxdb.annotation.TimeColumn;
 import org.influxdb.impl.Preconditions;
 
 /**
@@ -259,6 +262,16 @@ public class Point {
       try {
         Object fieldValue = field.get(pojo);
 
+        TimeColumn tc = field.getAnnotation(TimeColumn.class);
+        if (tc != null && Instant.class.isAssignableFrom(field.getType())) {
+          Optional.ofNullable((Instant) fieldValue).ifPresent(instant -> {
+            TimeUnit timeUint = tc.timeUnit();
+            this.time = TimeUnit.MILLISECONDS.convert(instant.toEpochMilli(), timeUint);
+            this.precision = timeUint;
+          });
+          return;
+        }
+
         if (column.tag()) {
           this.tags.put(fieldName, (String) fieldValue);
         } else {


Test patch:
diff --git a/src/test/java/org/influxdb/dto/PointTest.java b/src/test/java/org/influxdb/dto/PointTest.java
index 9478323..d1a9621 100644
--- a/src/test/java/org/influxdb/dto/PointTest.java
+++ b/src/test/java/org/influxdb/dto/PointTest.java
@@ -1,6 +1,8 @@
 package org.influxdb.dto;
 
 import static org.assertj.core.api.Assertions.assertThat;
+
+import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.Instant;
@@ -14,6 +16,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import org.influxdb.BuilderException;
 import org.influxdb.annotation.Column;
 import org.influxdb.annotation.Measurement;
+import org.influxdb.annotation.TimeColumn;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
@@ -511,7 +514,42 @@ public class PointTest {
   }
 
   @Test
-  public void testAddFieldsFromPOJOWithData() {
+  public void testAddFieldsFromPOJOWithTimeColumn() throws NoSuchFieldException, IllegalAccessException {
+    TimeColumnPojo pojo = new TimeColumnPojo();
+    pojo.time = Instant.now();
+    pojo.booleanPrimitive = true;
+
+    Point p = Point.measurementByPOJO(pojo.getClass()).addFieldsFromPOJO(pojo).build();
+    Field timeField = p.getClass().getDeclaredField("time");
+    Field precisionField = p.getClass().getDeclaredField("precision");
+    timeField.setAccessible(true);
+    precisionField.setAccessible(true);
+
+    Assertions.assertEquals(pojo.booleanPrimitive, p.getFields().get("booleanPrimitive"));
+    Assertions.assertEquals(TimeUnit.MILLISECONDS, precisionField.get(p));
+    Assertions.assertEquals(TimeUnit.MILLISECONDS.convert(pojo.time.toEpochMilli(),TimeUnit.MILLISECONDS), timeField.get(p));
+
+    pojo.time = null;
+  }
+
+	@Test
+	public void testAddFieldsFromPOJOWithTimeColumnNull() throws NoSuchFieldException, IllegalAccessException {
+		TimeColumnPojo pojo = new TimeColumnPojo();
+		pojo.booleanPrimitive = true;
+
+		Point p = Point.measurementByPOJO(pojo.getClass()).addFieldsFromPOJO(pojo).build();
+		Field timeField = p.getClass().getDeclaredField("time");
+		Field precisionField = p.getClass().getDeclaredField("precision");
+		timeField.setAccessible(true);
+		precisionField.setAccessible(true);
+
+		Assertions.assertEquals(pojo.booleanPrimitive, p.getFields().get("booleanPrimitive"));
+
+		pojo.time = null;
+	}
+
+  @Test
+  public void testAddFieldsFromPOJOWithData() throws NoSuchFieldException, IllegalAccessException {
     Pojo pojo = new Pojo();
     pojo.booleanObject = true;
     pojo.booleanPrimitive = false;
@@ -594,6 +632,15 @@ public class PointTest {
     }
   }
 
+	@Measurement(name = "tcmeasurement")
+  static class TimeColumnPojo {
+	  @Column(name = "booleanPrimitive")
+	  private boolean booleanPrimitive;
+
+	  @TimeColumn
+	  @Column(name = "time")
+	  private Instant time;
+  }
 
   @Measurement(name = "mymeasurement")
   static class Pojo {
diff --git a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java
index 7a14d46..f4eee0b 100644
--- a/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java
+++ b/src/test/java/org/influxdb/impl/InfluxDBResultMapperTest.java
@@ -34,6 +34,7 @@ import java.util.concurrent.TimeUnit;
 import org.influxdb.InfluxDBMapperException;
 import org.influxdb.annotation.Column;
 import org.influxdb.annotation.Measurement;
+import org.influxdb.annotation.TimeColumn;
 import org.influxdb.dto.QueryResult;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -441,6 +442,38 @@ public class InfluxDBResultMapperTest {
     Assertions.assertEquals(subValue, result.get(0).subValue);
   }
 
+  @Test
+  public void testToPOJO_HasTimeColumn() {
+	  // Given...
+	  mapper.cacheMeasurementClass(HasTimeColumnMeasurement.class);
+
+	  List<String> columnList = Arrays.asList("time");
+	  List<List<Object>> valuesList = Arrays.asList(
+			  Arrays.asList("2015-08-17T19:00:00-05:00"), // Chicago (UTC-5)
+			  Arrays.asList("2015-08-17T19:00:00.000000001-05:00"), // Chicago (UTC-5)
+			  Arrays.asList("2000-01-01T00:00:00-00:00"),
+			  Arrays.asList("2000-01-02T00:00:00+00:00")
+	  );
+
+	  QueryResult.Series series = new QueryResult.Series();
+	  series.setColumns(columnList);
+	  series.setValues(valuesList);
+
+	  // When...
+	  List<HasTimeColumnMeasurement> result = new LinkedList<>();
+	  mapper.parseSeriesAs(series, HasTimeColumnMeasurement.class, result);
+
+	  // Then...
+	  Assertions.assertEquals(4, result.size(), "incorrect number of elemets");
+	  // Note: RFC3339 timestamp with TZ from InfluxDB are parsed into an Instant (UTC)
+	  Assertions.assertTrue(result.get(0).time.equals(Instant.parse("2015-08-18T00:00:00Z")));
+	  Assertions.assertTrue(result.get(1).time.equals(Instant.parse("2015-08-18T00:00:00.000000001Z")));
+	  // RFC3339 section 4.3 https://tools.ietf.org/html/rfc3339#section-4.3
+	  Assertions.assertTrue(result.get(2).time.equals(Instant.parse("2000-01-01T00:00:00Z")));
+	  Assertions.assertTrue(result.get(3).time.equals(Instant.parse("2000-01-02T00:00:00Z")));
+
+  }
+
   @Test
   public void testToPOJO_ticket573() {
     // Given...
@@ -472,9 +505,35 @@ public class InfluxDBResultMapperTest {
     Assertions.assertTrue(result.get(3).time.equals(Instant.parse("2000-01-02T00:00:00Z")));
   }
 
+	@Measurement(name = "HasTimeColumnMeasurement")
+	static class HasTimeColumnMeasurement {
+  		@TimeColumn
+		@Column(name = "time")
+		private Instant time;
+
+  		@Column(name = "value")
+  		private Integer value;
+
+		public Instant getTime() {
+			return time;
+		}
+
+		public void setTime(Instant time) {
+			this.time = time;
+		}
+
+		public Integer getValue() {
+			return value;
+		}
+
+		public void setValue(Integer value) {
+			this.value = value;
+		}
+	}
+
+
 	@Measurement(name = "CustomMeasurement")
 	static class MyCustomMeasurement {
-
 		@Column(name = "time")
 		private Instant time;
 
@@ -555,7 +614,6 @@ public class InfluxDBResultMapperTest {
    */
   @Measurement(name = "tb_network")
   static class GroupByCarrierDeviceOS {
-
     @Column(name = "time")
     private Instant time;