diff --git a/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializer.java b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializer.java new file mode 100644 index 00000000..9d2b9f7a --- /dev/null +++ b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializer.java @@ -0,0 +1,175 @@ +package io.opengemini.client.spring.data.core; + +import io.opengemini.client.api.OpenGeminiException; +import io.opengemini.client.api.Point; +import io.opengemini.client.api.Precision; +import io.opengemini.client.spring.data.annotation.Measurement; +import io.opengemini.client.spring.data.annotation.Tag; +import io.opengemini.client.spring.data.annotation.Time; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class DefaultOpenGeminiSerializer implements OpenGeminiSerializer { + + private final ClassMetaData classMetaData; + + private DefaultOpenGeminiSerializer(ClassMetaData classMetaData) { + this.classMetaData = classMetaData; + } + + public static DefaultOpenGeminiSerializer of(Class clazz) { + ClassMetaData classMetaData = parseClassMetaData(clazz); + return new DefaultOpenGeminiSerializer<>(classMetaData); + } + + private static ClassMetaData parseClassMetaData(Class clazz) { + Measurement msAnnotation = clazz.getAnnotation(Measurement.class); + if (msAnnotation == null) { + throw new IllegalArgumentException("Class " + clazz.getName() + " has no @Measurement annotation"); + } + + ClassMetaData classMetaData = new ClassMetaData(msAnnotation.name()); + for (Field field : clazz.getDeclaredFields()) { + Tag tagAnnotation = field.getAnnotation(Tag.class); + if (tagAnnotation != null) { + if (!String.class.equals(field.getType())) { + throw new IllegalArgumentException( + "The " + field.getName() + " field type annotated with @Tag in Class " + clazz.getName() + + " should have a data type of String"); + } + classMetaData.putTag(tagAnnotation, field); + continue; + } + + io.opengemini.client.spring.data.annotation.Field fieldAnnotation = field.getAnnotation( + io.opengemini.client.spring.data.annotation.Field.class); + if (fieldAnnotation != null) { + classMetaData.putField(fieldAnnotation, field); + continue; + } + + Time timeAnnotation = field.getAnnotation(Time.class); + if (timeAnnotation != null) { + classMetaData.putTime(timeAnnotation, field); + } + } + return classMetaData; + } + + @Override + public Point serialize(T t) throws OpenGeminiException { + Point point = new Point(); + point.setMeasurement(classMetaData.measurementName); + point.setFields(new HashMap<>()); + point.setTags(new HashMap<>()); + + try { + for (Map.Entry entry : classMetaData.fieldMap.entrySet()) { + String columnName = entry.getKey(); + FieldMetaData fieldMetaData = entry.getValue(); + switch (fieldMetaData.fieldType) { + case TAG: + point.getTags().put(columnName, fieldMetaData.parseTagValue(t)); + break; + case FIELD: + point.getFields().put(columnName, fieldMetaData.parseFieldValue(t)); + break; + case TIME: + point.setTime(fieldMetaData.parseTimeValue(t)); + point.setPrecision(fieldMetaData.timePrecision); + break; + default: + break; + } + } + return point; + } catch (IllegalAccessException e) { + throw new OpenGeminiException(e); + } + } + + private enum FieldType { + TAG, FIELD, TIME + } + + private static class ClassMetaData { + private final String measurementName; + private final Map fieldMap; + + public ClassMetaData(String measurementName) { + this.measurementName = measurementName; + this.fieldMap = new HashMap<>(); + } + + public void putTag(Tag tagAnnotation, Field field) { + field.setAccessible(true); + fieldMap.put(getTagName(tagAnnotation, field), FieldMetaData.tag(field)); + } + + public void putField(io.opengemini.client.spring.data.annotation.Field fieldAnnotation, Field field) { + field.setAccessible(true); + fieldMap.put(getFieldName(fieldAnnotation, field), FieldMetaData.field(field)); + } + + public void putTime(Time timeAnnotation, Field field) { + field.setAccessible(true); + fieldMap.put("time", FieldMetaData.time(field, timeAnnotation.precision())); + } + + private String getTagName(Tag tagAnnotation, Field field) { + return tagAnnotation.name().isEmpty() ? field.getName() : tagAnnotation.name(); + } + + private String getFieldName(io.opengemini.client.spring.data.annotation.Field fieldAnnotation, Field field) { + return fieldAnnotation.name().isEmpty() ? field.getName() : fieldAnnotation.name(); + } + } + + private static class FieldMetaData { + private Field field; + private FieldType fieldType; + private Precision timePrecision; + + public static FieldMetaData tag(Field field) { + FieldMetaData fieldMetaData = new FieldMetaData(); + fieldMetaData.field = field; + fieldMetaData.fieldType = FieldType.TAG; + return fieldMetaData; + } + + public static FieldMetaData field(Field field) { + FieldMetaData fieldMetaData = new FieldMetaData(); + fieldMetaData.field = field; + fieldMetaData.fieldType = FieldType.FIELD; + return fieldMetaData; + } + + public static FieldMetaData time(Field field, Precision timePrecision) { + FieldMetaData fieldMetaData = new FieldMetaData(); + fieldMetaData.field = field; + fieldMetaData.fieldType = FieldType.TIME; + fieldMetaData.timePrecision = timePrecision; + return fieldMetaData; + } + + public String parseTagValue(Object obj) throws IllegalAccessException { + return Objects.toString(field.get(obj), null); + } + + public Object parseFieldValue(Object obj) throws IllegalAccessException { + return field.get(obj); + } + + public long parseTimeValue(Object obj) throws IllegalAccessException { + Object fieldValue = field.get(obj); + if (fieldValue instanceof Long) { + return (Long) fieldValue; + } + return timePrecision.getTimeUnit().convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + } +} diff --git a/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializer.java b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializer.java new file mode 100644 index 00000000..ccf03cf7 --- /dev/null +++ b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializer.java @@ -0,0 +1,13 @@ +package io.opengemini.client.spring.data.core; + +import io.opengemini.client.api.OpenGeminiException; +import io.opengemini.client.api.Point; + +/** + * The serializer between POJO and Point + * + * @param class type of POJO annotated by Measurement + */ +public interface OpenGeminiSerializer { + Point serialize(T t) throws OpenGeminiException; +} diff --git a/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerTest.java b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerTest.java new file mode 100644 index 00000000..2610e397 --- /dev/null +++ b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerTest.java @@ -0,0 +1,49 @@ +package io.opengemini.client.spring.data.core; + +import io.opengemini.client.api.Point; +import io.opengemini.client.spring.data.sample.measurement.WeatherFixNameNoCreate; +import io.opengemini.client.spring.data.sample.measurement.WeatherNoAnnotation; +import io.opengemini.client.spring.data.sample.measurement.WeatherTagNotString; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +class DefaultOpenGeminiSerializerTest { + + @Test + void serializer_should_fail_when_pojo_not_annotated() { + Executable executable = () -> DefaultOpenGeminiSerializer.of(WeatherNoAnnotation.class); + IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, executable); + Assertions.assertEquals( + "Class io.opengemini.client.spring.data.sample.measurement.WeatherNoAnnotation has no @Measurement " + + "annotation", exception.getMessage()); + } + + @Test + void serializer_should_fail_when_tag_not_string() { + Executable executable = () -> DefaultOpenGeminiSerializer.of(WeatherTagNotString.class); + IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, executable); + Assertions.assertEquals( + "The location field type annotated with @Tag in Class io.opengemini.client.spring.data.sample" + + ".measurement.WeatherTagNotString should have a data type of String", exception.getMessage()); + } + + @Test + void serialize_should_success_when_pojo_annotated() throws Exception { + OpenGeminiSerializer serializer = DefaultOpenGeminiSerializer.of( + WeatherFixNameNoCreate.class); + + WeatherFixNameNoCreate weather = new WeatherFixNameNoCreate(); + weather.setLocation("shenzhen"); + weather.setTemperature(28.5D); + weather.setTime(1725371248720L); + + Point point = serializer.serialize(weather); + Assertions.assertNotNull(point); + Assertions.assertEquals("testms", point.getMeasurement()); + Assertions.assertEquals("shenzhen", point.getTags().get("Location")); + Assertions.assertEquals(28.5D, point.getFields().get("Temperature")); + Assertions.assertEquals(weather.getTime(), point.getTime()); + Assertions.assertEquals("testms,Location=shenzhen Temperature=28.5 1725371248720000000", point.lineProtocol()); + } +} diff --git a/spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherFixNameNoCreate.java b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherFixNameNoCreate.java similarity index 99% rename from spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherFixNameNoCreate.java rename to spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherFixNameNoCreate.java index deb43607..03ab4a4b 100644 --- a/spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherFixNameNoCreate.java +++ b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherFixNameNoCreate.java @@ -1,6 +1,5 @@ package io.opengemini.client.spring.data.sample.measurement; - import io.opengemini.client.api.Precision; import io.opengemini.client.spring.data.annotation.Database; import io.opengemini.client.spring.data.annotation.Field; diff --git a/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherNoAnnotation.java b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherNoAnnotation.java new file mode 100644 index 00000000..1cf5724b --- /dev/null +++ b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherNoAnnotation.java @@ -0,0 +1,23 @@ +package io.opengemini.client.spring.data.sample.measurement; + +import io.opengemini.client.api.Precision; +import io.opengemini.client.spring.data.annotation.Field; +import io.opengemini.client.spring.data.annotation.Tag; +import io.opengemini.client.spring.data.annotation.Time; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WeatherNoAnnotation { + + @Tag(name = "Location") + private String location; + + @Field(name = "Temperature") + private Double temperature; + + @Time(precision = Precision.PRECISIONMILLISECOND) + private Long time; + +} diff --git a/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherTagNotString.java b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherTagNotString.java new file mode 100644 index 00000000..1f1f8431 --- /dev/null +++ b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/sample/measurement/WeatherTagNotString.java @@ -0,0 +1,29 @@ +package io.opengemini.client.spring.data.sample.measurement; + +import io.opengemini.client.api.Precision; +import io.opengemini.client.spring.data.annotation.Database; +import io.opengemini.client.spring.data.annotation.Field; +import io.opengemini.client.spring.data.annotation.Measurement; +import io.opengemini.client.spring.data.annotation.RetentionPolicy; +import io.opengemini.client.spring.data.annotation.Tag; +import io.opengemini.client.spring.data.annotation.Time; +import lombok.Getter; +import lombok.Setter; + +@Database(name = "testdb", create = false) +@RetentionPolicy(name = "testrp", create = false) +@Measurement(name = "testms") +@Getter +@Setter +public class WeatherTagNotString { + + @Tag(name = "Location") + private Long location; + + @Field(name = "Temperature") + private Double temperature; + + @Time(precision = Precision.PRECISIONMILLISECOND) + private Long time; + +}