From 5a94a07676506467a50530ea039510a2b6235d5c Mon Sep 17 00:00:00 2001 From: weiping-code Date: Sat, 28 Sep 2024 20:21:43 +0800 Subject: [PATCH] feat: add spring pojo deserialize method (#129) Signed-off-by: weiping-code <54316849+weiping-code@users.noreply.github.com> --- .../OpenGeminiReactiveAutoConfiguration.java | 7 + ...enGeminiReactiveAutoConfigurationTest.java | 9 + .../config/OpenGeminiAutoConfiguration.java | 8 + .../OpenGeminiAutoConfigurationTest.java | 9 + .../core/DefaultOpenGeminiSerializer.java | 369 ++++++++++++++---- .../DefaultOpenGeminiSerializerFactory.java | 15 + .../data/core/OpenGeminiSerializer.java | 6 + .../core/OpenGeminiSerializerFactory.java | 5 + ...efaultOpenGeminiSerializerFactoryTest.java | 37 ++ .../core/DefaultOpenGeminiSerializerTest.java | 49 ++- 10 files changed, 438 insertions(+), 76 deletions(-) create mode 100644 spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactory.java create mode 100644 spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializerFactory.java create mode 100644 spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactoryTest.java diff --git a/spring/opengemini-spring-boot-starter-reactive/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfiguration.java b/spring/opengemini-spring-boot-starter-reactive/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfiguration.java index a40e54cb..f6630356 100644 --- a/spring/opengemini-spring-boot-starter-reactive/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfiguration.java +++ b/spring/opengemini-spring-boot-starter-reactive/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfiguration.java @@ -1,5 +1,7 @@ package io.opengemini.client.spring.data.config; +import io.opengemini.client.spring.data.core.DefaultOpenGeminiSerializerFactory; +import io.opengemini.client.spring.data.core.OpenGeminiSerializerFactory; import io.opengemini.client.spring.data.core.ReactiveOpenGeminiTemplate; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -17,4 +19,9 @@ public ReactiveOpenGeminiTemplate reactiveOpenGeminiTemplate() { return new ReactiveOpenGeminiTemplate(); } + @Bean + @ConditionalOnMissingBean(name = "openGeminiSerializerFactory") + public OpenGeminiSerializerFactory openGeminiSerializerFactory() { + return new DefaultOpenGeminiSerializerFactory(); + } } diff --git a/spring/opengemini-spring-boot-starter-reactive/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfigurationTest.java b/spring/opengemini-spring-boot-starter-reactive/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfigurationTest.java index 2fd68bf4..58e6c9a7 100644 --- a/spring/opengemini-spring-boot-starter-reactive/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfigurationTest.java +++ b/spring/opengemini-spring-boot-starter-reactive/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiReactiveAutoConfigurationTest.java @@ -1,6 +1,7 @@ package io.opengemini.client.spring.data.config; import io.opengemini.client.spring.data.core.OpenGeminiProperties; +import io.opengemini.client.spring.data.core.OpenGeminiSerializerFactory; import io.opengemini.client.spring.data.core.ReactiveOpenGeminiTemplate; import io.opengemini.client.spring.data.sample.TestReactiveApplication; import org.junit.jupiter.api.Assertions; @@ -19,6 +20,9 @@ public class OpenGeminiReactiveAutoConfigurationTest { @Autowired private ReactiveOpenGeminiTemplate reactiveOpenGeminiTemplate; + @Autowired + private OpenGeminiSerializerFactory openGeminiSerializerFactory; + @Test public void properties_bean_should_be_declared() { Assertions.assertNotNull(openGeminiProperties); @@ -30,4 +34,9 @@ public void reactive_template_bean_should_be_declared() { Assertions.assertNotNull(reactiveOpenGeminiTemplate); } + @Test + public void serializerFactory_bean_should_be_declared() { + Assertions.assertNotNull(openGeminiSerializerFactory); + } + } diff --git a/spring/opengemini-spring-boot-starter/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfiguration.java b/spring/opengemini-spring-boot-starter/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfiguration.java index 0859f81d..0e086494 100644 --- a/spring/opengemini-spring-boot-starter/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfiguration.java +++ b/spring/opengemini-spring-boot-starter/src/main/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfiguration.java @@ -1,6 +1,8 @@ package io.opengemini.client.spring.data.config; +import io.opengemini.client.spring.data.core.DefaultOpenGeminiSerializerFactory; import io.opengemini.client.spring.data.core.OpenGeminiProperties; +import io.opengemini.client.spring.data.core.OpenGeminiSerializerFactory; import io.opengemini.client.spring.data.core.OpenGeminiTemplate; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -19,4 +21,10 @@ public OpenGeminiTemplate openGeminiTemplate() { return new OpenGeminiTemplate(); } + @Bean + @ConditionalOnMissingBean(name = "openGeminiSerializerFactory") + public OpenGeminiSerializerFactory openGeminiSerializerFactory() { + return new DefaultOpenGeminiSerializerFactory(); + } + } diff --git a/spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfigurationTest.java b/spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfigurationTest.java index 79e64de8..65a17ce7 100644 --- a/spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfigurationTest.java +++ b/spring/opengemini-spring-boot-starter/src/test/java/io/opengemini/client/spring/data/config/OpenGeminiAutoConfigurationTest.java @@ -1,6 +1,7 @@ package io.opengemini.client.spring.data.config; import io.opengemini.client.spring.data.core.OpenGeminiProperties; +import io.opengemini.client.spring.data.core.OpenGeminiSerializerFactory; import io.opengemini.client.spring.data.core.OpenGeminiTemplate; import io.opengemini.client.spring.data.sample.TestApplication; import org.junit.jupiter.api.Assertions; @@ -19,6 +20,9 @@ public class OpenGeminiAutoConfigurationTest { @Autowired private OpenGeminiTemplate openGeminiTemplate; + @Autowired + private OpenGeminiSerializerFactory openGeminiSerializerFactory; + @Test public void properties_bean_should_be_declared() { Assertions.assertNotNull(openGeminiProperties); @@ -30,4 +34,9 @@ public void template_bean_should_be_declared() { Assertions.assertNotNull(openGeminiTemplate); } + @Test + public void serializerFactory_bean_should_be_declared() { + Assertions.assertNotNull(openGeminiSerializerFactory); + } + } 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 index 9d2b9f7a..b9607c6b 100644 --- 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 @@ -3,88 +3,95 @@ import io.opengemini.client.api.OpenGeminiException; import io.opengemini.client.api.Point; import io.opengemini.client.api.Precision; +import io.opengemini.client.api.QueryResult; +import io.opengemini.client.api.Series; +import io.opengemini.client.api.SeriesResult; 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 lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; public class DefaultOpenGeminiSerializer implements OpenGeminiSerializer { - private final ClassMetaData classMetaData; + private static final DateTimeFormatter RFC3339_FORMATTER = new DateTimeFormatterBuilder().appendPattern( + "yyyy-MM-dd'T'HH:mm:ss") + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .appendZoneOrOffsetId() + .toFormatter(); - private DefaultOpenGeminiSerializer(ClassMetaData classMetaData) { + private final ClassMetaData classMetaData; + + private DefaultOpenGeminiSerializer(ClassMetaData classMetaData) { this.classMetaData = classMetaData; } public static DefaultOpenGeminiSerializer of(Class clazz) { - ClassMetaData classMetaData = parseClassMetaData(clazz); + ClassMetaData classMetaData = parseClassMetaData(clazz); return new DefaultOpenGeminiSerializer<>(classMetaData); } - private static ClassMetaData parseClassMetaData(Class clazz) { + 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()); + Map fieldMap = new HashMap<>(); 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); + TagFieldMetaData fieldMetaData = TagFieldMetaData.of(clazz, tagAnnotation, field); + fieldMap.put(fieldMetaData.getName(), fieldMetaData); 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); + ConcreteFieldMetaData fieldMetaData = ConcreteFieldMetaData.of(clazz, fieldAnnotation, field); + fieldMap.put(fieldMetaData.getName(), fieldMetaData); continue; } Time timeAnnotation = field.getAnnotation(Time.class); if (timeAnnotation != null) { - classMetaData.putTime(timeAnnotation, field); + TimeFieldMetaData fieldMetaData = TimeFieldMetaData.of(clazz, timeAnnotation, field); + fieldMap.put(fieldMetaData.getName(), fieldMetaData); } } - return classMetaData; + return new ClassMetaData<>(clazz, msAnnotation.name(), fieldMap); } @Override public Point serialize(T t) throws OpenGeminiException { Point point = new Point(); - point.setMeasurement(classMetaData.measurementName); + point.setMeasurement(classMetaData.getMeasurementName()); 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; - } + for (AbstractFieldMetaData fieldMetaData : classMetaData.getFieldMap().values()) { + fieldMetaData.fillPoint(point, t); } return point; } catch (IllegalAccessException e) { @@ -92,84 +99,296 @@ public Point serialize(T t) throws OpenGeminiException { } } - private enum FieldType { - TAG, FIELD, TIME + @Override + public List deserialize(QueryResult queryResult) throws OpenGeminiException { + validateResultNoError(queryResult); + + List pojoList = new LinkedList<>(); + for (SeriesResult seriesResult : queryResult.getResults()) { + if (seriesResult == null || CollectionUtils.isEmpty(seriesResult.getSeries())) { + continue; + } + for (Series series : seriesResult.getSeries()) { + if (!StringUtils.equals(series.getName(), classMetaData.getMeasurementName())) { + continue; + } + pojoList.addAll(parseSeriesAsPojoList(series)); + } + } + + return pojoList; + } + + private Collection parseSeriesAsPojoList(Series series) throws OpenGeminiException { + List pojoList = new LinkedList<>(); + int columnSize = series.getColumns().size(); + try { + T pojo = null; + for (List row : series.getValues()) { + for (int i = 0; i < columnSize; i++) { + AbstractFieldMetaData correspondingField = classMetaData.getFieldMap() + .get(series.getColumns().get(i)); + if (correspondingField != null) { + if (pojo == null) { + pojo = classMetaData.newInstance(); + } + setFieldValue(pojo, correspondingField, row.get(i)); + } + } + if (series.getTags() != null && !series.getTags().isEmpty()) { + for (Map.Entry entry : series.getTags().entrySet()) { + AbstractFieldMetaData correspondingField = classMetaData.getFieldMap().get(entry.getKey()); + if (correspondingField != null) { + setFieldValue(pojo, correspondingField, entry.getValue()); + } + } + } + if (pojo != null) { + pojoList.add(pojo); + pojo = null; + } + } + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException + | InvocationTargetException e) { + throw new OpenGeminiException(e); + } + return pojoList; } - private static class ClassMetaData { + private void setFieldValue(T pojo, AbstractFieldMetaData fieldMetaData, Object fieldValue) + throws OpenGeminiException { + if (fieldValue == null) { + return; + } + try { + fieldMetaData.setFieldValue(pojo, fieldValue); + } catch (IllegalAccessException e) { + throw new OpenGeminiException(e); + } + } + + private void validateResultNoError(QueryResult queryResult) throws OpenGeminiException { + if (queryResult.getError() != null) { + throw new OpenGeminiException("InfluxDB returned an error: " + queryResult.getError()); + } + + for (SeriesResult seriesResult : queryResult.getResults()) { + if (seriesResult != null && seriesResult.getError() != null) { + throw new OpenGeminiException("InfluxDB returned an error with Series: " + seriesResult.getError()); + } + } + } + + @Getter + private static class ClassMetaData { + private final Class clazz; private final String measurementName; - private final Map fieldMap; + private final Map fieldMap; - public ClassMetaData(String measurementName) { + public ClassMetaData(Class clazz, String measurementName, Map fieldMap) { + this.clazz = clazz; this.measurementName = measurementName; - this.fieldMap = new HashMap<>(); + this.fieldMap = fieldMap; } - public void putTag(Tag tagAnnotation, Field field) { - field.setAccessible(true); - fieldMap.put(getTagName(tagAnnotation, field), FieldMetaData.tag(field)); + public T newInstance() throws NoSuchMethodException, InvocationTargetException, InstantiationException, + IllegalAccessException { + return clazz.getDeclaredConstructor().newInstance(); + } + } + + @Getter + private abstract static class AbstractFieldMetaData { + + protected final String name; + protected final Field field; + + public AbstractFieldMetaData(String name, Field field) { + this.name = name; + this.field = field; + } + + protected static void validateFieldType(Class clazz, + Field field, + Set> validClasses, + Class annotationClazz) { + if (!validClasses.contains(field.getType())) { + throw new IllegalArgumentException( + "The " + field.getName() + " field type annotated with @" + annotationClazz.getSimpleName() + + " in Class " + clazz.getName() + " should have a data type of " + validClasses); + } + } + + public abstract void fillPoint(Point point, Object pojo) throws IllegalAccessException; + + public void setFieldValue(Object pojo, Object fieldValue) throws IllegalAccessException { + Object convertedValue = convertFieldValue(fieldValue); + if (convertedValue != null) { + field.set(pojo, convertedValue); + } + } + + public abstract Object convertFieldValue(Object fieldValue); + + } + + private static class TagFieldMetaData extends AbstractFieldMetaData { + + private static final Set> VALID_CLASSES = new HashSet<>(); + + static { + VALID_CLASSES.add(String.class); + } + + public TagFieldMetaData(String name, Field field) { + super(name, field); } - public void putField(io.opengemini.client.spring.data.annotation.Field fieldAnnotation, Field field) { + public static TagFieldMetaData of(Class clazz, Tag tagAnnotation, Field field) { + validateFieldType(clazz, field, VALID_CLASSES, Tag.class); + + String tagName = tagAnnotation.name().isEmpty() ? field.getName() : tagAnnotation.name(); field.setAccessible(true); - fieldMap.put(getFieldName(fieldAnnotation, field), FieldMetaData.field(field)); + return new TagFieldMetaData(tagName, field); + } + + @Override + public void fillPoint(Point point, Object pojo) throws IllegalAccessException { + point.getTags().put(name, Objects.toString(field.get(pojo), null)); + } + + @Override + public Object convertFieldValue(Object fieldValue) { + return String.valueOf(fieldValue); + } + } + + private static class ConcreteFieldMetaData extends AbstractFieldMetaData { + + private static final Set> VALID_CLASSES = new HashSet<>(); + + static { + VALID_CLASSES.add(String.class); + VALID_CLASSES.add(Double.class); + VALID_CLASSES.add(double.class); + VALID_CLASSES.add(Float.class); + VALID_CLASSES.add(float.class); + VALID_CLASSES.add(Long.class); + VALID_CLASSES.add(long.class); + VALID_CLASSES.add(Integer.class); + VALID_CLASSES.add(int.class); + VALID_CLASSES.add(BigDecimal.class); + VALID_CLASSES.add(BigInteger.class); + VALID_CLASSES.add(Boolean.class); + VALID_CLASSES.add(boolean.class); } - public void putTime(Time timeAnnotation, Field field) { + public ConcreteFieldMetaData(String name, Field field) { + super(name, field); + } + + public static ConcreteFieldMetaData of(Class clazz, + io.opengemini.client.spring.data.annotation.Field fieldAnnotation, + Field field) { + validateFieldType(clazz, field, VALID_CLASSES, io.opengemini.client.spring.data.annotation.Field.class); + + String fieldName = fieldAnnotation.name().isEmpty() ? field.getName() : fieldAnnotation.name(); field.setAccessible(true); - fieldMap.put("time", FieldMetaData.time(field, timeAnnotation.precision())); + return new ConcreteFieldMetaData(fieldName, field); } - private String getTagName(Tag tagAnnotation, Field field) { - return tagAnnotation.name().isEmpty() ? field.getName() : tagAnnotation.name(); + @Override + public void fillPoint(Point point, Object pojo) throws IllegalAccessException { + point.getFields().put(name, field.get(pojo)); } - private String getFieldName(io.opengemini.client.spring.data.annotation.Field fieldAnnotation, Field field) { - return fieldAnnotation.name().isEmpty() ? field.getName() : fieldAnnotation.name(); + @Override + public Object convertFieldValue(Object fieldValue) { + Class fieldType = field.getType(); + if (String.class.isAssignableFrom(fieldType)) { + return String.valueOf(fieldValue); + } + if (Double.class.isAssignableFrom(fieldType) || double.class.isAssignableFrom(fieldType)) { + return fieldValue; + } + if (Float.class.isAssignableFrom(fieldType) || float.class.isAssignableFrom(fieldType)) { + return ((Double) fieldValue).floatValue(); + } + if (BigDecimal.class.isAssignableFrom(fieldType)) { + return BigDecimal.valueOf((Double) fieldValue); + } + if (Long.class.isAssignableFrom(fieldType) || long.class.isAssignableFrom(fieldType)) { + return ((Double) fieldValue).longValue(); + } + if (Integer.class.isAssignableFrom(fieldType) || int.class.isAssignableFrom(fieldType)) { + return ((Double) fieldValue).intValue(); + } + if (BigInteger.class.isAssignableFrom(fieldType)) { + return BigInteger.valueOf(((Double) fieldValue).longValue()); + } + if (Boolean.class.isAssignableFrom(fieldType) || boolean.class.isAssignableFrom(fieldType)) { + return Boolean.valueOf(String.valueOf(fieldValue)); + } + return null; } } - private static class FieldMetaData { - private Field field; - private FieldType fieldType; - private Precision timePrecision; + private static class TimeFieldMetaData extends AbstractFieldMetaData { - public static FieldMetaData tag(Field field) { - FieldMetaData fieldMetaData = new FieldMetaData(); - fieldMetaData.field = field; - fieldMetaData.fieldType = FieldType.TAG; - return fieldMetaData; + private static final Set> VALID_CLASSES = new HashSet<>(); + + static { + VALID_CLASSES.add(Long.class); + VALID_CLASSES.add(long.class); } - public static FieldMetaData field(Field field) { - FieldMetaData fieldMetaData = new FieldMetaData(); - fieldMetaData.field = field; - fieldMetaData.fieldType = FieldType.FIELD; - return fieldMetaData; + private final Precision timePrecision; + + public TimeFieldMetaData(String name, Field field, Precision timePrecision) { + super(name, field); + this.timePrecision = timePrecision; } - 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 static TimeFieldMetaData of(Class clazz, Time timeAnnotation, Field field) { + validateFieldType(clazz, field, VALID_CLASSES, Time.class); + + field.setAccessible(true); + return new TimeFieldMetaData("time", field, timeAnnotation.precision()); } - public String parseTagValue(Object obj) throws IllegalAccessException { - return Objects.toString(field.get(obj), null); + @Override + public void fillPoint(Point point, Object pojo) throws IllegalAccessException { + point.setTime(parseTimeValueFromPojo(pojo)); + point.setPrecision(timePrecision); } - public Object parseFieldValue(Object obj) throws IllegalAccessException { - return field.get(obj); + @Override + public Object convertFieldValue(Object fieldValue) { + Long nanos = parseNanoTimeValueFromResult(fieldValue); + if (nanos != null) { + return timePrecision.getTimeUnit().convert(nanos, TimeUnit.NANOSECONDS); + } + return null; } - public long parseTimeValue(Object obj) throws IllegalAccessException { + private long parseTimeValueFromPojo(Object obj) throws IllegalAccessException { Object fieldValue = field.get(obj); + if (fieldValue == null) { + return timePrecision.getTimeUnit().convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } else { + return (Long) fieldValue; + } + } + + private Long parseNanoTimeValueFromResult(Object fieldValue) { + if (fieldValue instanceof String) { + Instant instant = Instant.from(RFC3339_FORMATTER.parse(String.valueOf(fieldValue))); + return TimeUnit.NANOSECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + } if (fieldValue instanceof Long) { return (Long) fieldValue; } - return timePrecision.getTimeUnit().convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + return null; } } } diff --git a/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactory.java b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactory.java new file mode 100644 index 00000000..2ac21f2a --- /dev/null +++ b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactory.java @@ -0,0 +1,15 @@ +package io.opengemini.client.spring.data.core; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class DefaultOpenGeminiSerializerFactory implements OpenGeminiSerializerFactory { + + private final Map, OpenGeminiSerializer> serializerMap = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public OpenGeminiSerializer getSerializer(Class clazz) { + return (OpenGeminiSerializer) serializerMap.computeIfAbsent(clazz, DefaultOpenGeminiSerializer::of); + } +} 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 index ccf03cf7..0c4cd51a 100644 --- 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 @@ -2,6 +2,9 @@ import io.opengemini.client.api.OpenGeminiException; import io.opengemini.client.api.Point; +import io.opengemini.client.api.QueryResult; + +import java.util.List; /** * The serializer between POJO and Point @@ -9,5 +12,8 @@ * @param class type of POJO annotated by Measurement */ public interface OpenGeminiSerializer { + Point serialize(T t) throws OpenGeminiException; + + List deserialize(QueryResult queryResult) throws OpenGeminiException; } diff --git a/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializerFactory.java b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializerFactory.java new file mode 100644 index 00000000..c3d2bdb5 --- /dev/null +++ b/spring/opengemini-spring/src/main/java/io/opengemini/client/spring/data/core/OpenGeminiSerializerFactory.java @@ -0,0 +1,5 @@ +package io.opengemini.client.spring.data.core; + +public interface OpenGeminiSerializerFactory { + OpenGeminiSerializer getSerializer(Class clazz); +} diff --git a/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactoryTest.java b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactoryTest.java new file mode 100644 index 00000000..083466b7 --- /dev/null +++ b/spring/opengemini-spring/src/test/java/io/opengemini/client/spring/data/core/DefaultOpenGeminiSerializerFactoryTest.java @@ -0,0 +1,37 @@ +package io.opengemini.client.spring.data.core; + +import io.opengemini.client.spring.data.sample.measurement.WeatherFixNameNoCreate; +import io.opengemini.client.spring.data.sample.measurement.WeatherNoAnnotation; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +class DefaultOpenGeminiSerializerFactoryTest { + + private OpenGeminiSerializerFactory factory; + + @BeforeEach + void setUp() { + factory = new DefaultOpenGeminiSerializerFactory(); + } + + @Test + void serializer_should_fail_when_pojo_not_annotated() { + Executable executable = () -> factory.getSerializer(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 getSerializer_should_success_when_pojo_annotated() { + OpenGeminiSerializer serializer1 = factory.getSerializer(WeatherFixNameNoCreate.class); + OpenGeminiSerializer serializer2 = factory.getSerializer(WeatherFixNameNoCreate.class); + + Assertions.assertNotNull(serializer1); + Assertions.assertNotNull(serializer2); + Assertions.assertSame(serializer1, serializer2); + } +} 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 index 2610e397..6cd63c8d 100644 --- 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 @@ -1,6 +1,10 @@ package io.opengemini.client.spring.data.core; +import io.opengemini.client.api.OpenGeminiException; import io.opengemini.client.api.Point; +import io.opengemini.client.api.QueryResult; +import io.opengemini.client.api.Series; +import io.opengemini.client.api.SeriesResult; 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; @@ -8,6 +12,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import java.util.Arrays; +import java.util.List; + class DefaultOpenGeminiSerializerTest { @Test @@ -25,7 +32,8 @@ void serializer_should_fail_when_tag_not_string() { 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()); + + ".measurement.WeatherTagNotString should have a data type of [class java.lang.String]", + exception.getMessage()); } @Test @@ -46,4 +54,43 @@ void serialize_should_success_when_pojo_annotated() throws Exception { Assertions.assertEquals(weather.getTime(), point.getTime()); Assertions.assertEquals("testms,Location=shenzhen Temperature=28.5 1725371248720000000", point.lineProtocol()); } + + @Test + void deserialize_should_throw_exception_when_queryResult_has_error() { + OpenGeminiSerializer serializer = DefaultOpenGeminiSerializer.of( + WeatherFixNameNoCreate.class); + + QueryResult queryResult = new QueryResult(); + queryResult.setError("some error"); + + Assertions.assertThrows(OpenGeminiException.class, () -> serializer.deserialize(queryResult)); + } + + @Test + void deserialize_should_success_when_queryResult_matches() throws Exception { + OpenGeminiSerializer serializer = DefaultOpenGeminiSerializer.of( + WeatherFixNameNoCreate.class); + + Series series = new Series(); + series.setName("testms"); + series.setColumns(Arrays.asList("Location", "Temperature", "time")); + series.setValues(List.of(Arrays.asList("shenzhen", 28.5D, 1725371248720000000L), + Arrays.asList("shanghai", 26.8D, 1725371248720000000L))); + + SeriesResult seriesResult = new SeriesResult(); + seriesResult.setSeries(List.of(series)); + + QueryResult queryResult = new QueryResult(); + queryResult.setResults(List.of(seriesResult)); + + List list = serializer.deserialize(queryResult); + + Assertions.assertEquals(2, list.size()); + Assertions.assertEquals("shenzhen", list.get(0).getLocation()); + Assertions.assertEquals(28.5D, list.get(0).getTemperature()); + Assertions.assertEquals(1725371248720L, list.get(0).getTime()); + Assertions.assertEquals("shanghai", list.get(1).getLocation()); + Assertions.assertEquals(26.8D, list.get(1).getTemperature()); + Assertions.assertEquals(1725371248720L, list.get(1).getTime()); + } }