You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@johnzon.apache.org by rm...@apache.org on 2021/06/20 14:28:48 UTC
[johnzon] 01/02: [JOHNZON-348] jsonb annotations support on records
This is an automated email from the ASF dual-hosted git repository.
rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git
commit cf2cd73c3b5ff85852ec34a7caeb483665506af3
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Sun Jun 20 16:27:23 2021 +0200
[JOHNZON-348] jsonb annotations support on records
---
.../org/apache/johnzon/jsonb/JsonbAccessMode.java | 227 ++++++++++++++-------
.../org/apache/johnzon/jsonb/JsonbRecordTest.java | 87 ++++++++
2 files changed, 236 insertions(+), 78 deletions(-)
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java
index cc03e7b..3204101 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbAccessMode.java
@@ -18,12 +18,62 @@
*/
package org.apache.johnzon.jsonb;
-import static java.util.Arrays.asList;
-import static java.util.Optional.ofNullable;
-import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toSet;
-import static org.apache.johnzon.mapper.reflection.Converters.matches;
+import org.apache.johnzon.core.Types;
+import org.apache.johnzon.jsonb.converter.JohnzonJsonbAdapter;
+import org.apache.johnzon.jsonb.converter.JsonbDateConverter;
+import org.apache.johnzon.jsonb.converter.JsonbLocalDateConverter;
+import org.apache.johnzon.jsonb.converter.JsonbLocalDateTimeConverter;
+import org.apache.johnzon.jsonb.converter.JsonbNumberConverter;
+import org.apache.johnzon.jsonb.converter.JsonbOffsetDateTimeConverter;
+import org.apache.johnzon.jsonb.converter.JsonbValueConverter;
+import org.apache.johnzon.jsonb.converter.JsonbZonedDateTimeConverter;
+import org.apache.johnzon.jsonb.order.PerHierarchyAndLexicographicalOrderFieldComparator;
+import org.apache.johnzon.jsonb.reflect.GenericArrayTypeImpl;
+import org.apache.johnzon.jsonb.serializer.JohnzonDeserializationContext;
+import org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext;
+import org.apache.johnzon.jsonb.spi.JohnzonAdapterFactory;
+import org.apache.johnzon.mapper.Adapter;
+import org.apache.johnzon.mapper.Converter;
+import org.apache.johnzon.mapper.JohnzonAny;
+import org.apache.johnzon.mapper.JohnzonConverter;
+import org.apache.johnzon.mapper.JohnzonRecord;
+import org.apache.johnzon.mapper.MapperConverter;
+import org.apache.johnzon.mapper.MappingGenerator;
+import org.apache.johnzon.mapper.MappingParser;
+import org.apache.johnzon.mapper.ObjectConverter;
+import org.apache.johnzon.mapper.TypeAwareAdapter;
+import org.apache.johnzon.mapper.access.AccessMode;
+import org.apache.johnzon.mapper.access.BaseAccessMode;
+import org.apache.johnzon.mapper.access.FieldAccessMode;
+import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
+import org.apache.johnzon.mapper.access.Meta;
+import org.apache.johnzon.mapper.access.MethodAccessMode;
+import org.apache.johnzon.mapper.converter.ReversedAdapter;
+import org.apache.johnzon.mapper.internal.AdapterKey;
+import org.apache.johnzon.mapper.internal.ConverterAdapter;
+import javax.json.JsonBuilderFactory;
+import javax.json.JsonValue;
+import javax.json.bind.JsonbException;
+import javax.json.bind.adapter.JsonbAdapter;
+import javax.json.bind.annotation.JsonbCreator;
+import javax.json.bind.annotation.JsonbDateFormat;
+import javax.json.bind.annotation.JsonbNillable;
+import javax.json.bind.annotation.JsonbNumberFormat;
+import javax.json.bind.annotation.JsonbProperty;
+import javax.json.bind.annotation.JsonbPropertyOrder;
+import javax.json.bind.annotation.JsonbTransient;
+import javax.json.bind.annotation.JsonbTypeAdapter;
+import javax.json.bind.annotation.JsonbTypeDeserializer;
+import javax.json.bind.annotation.JsonbTypeSerializer;
+import javax.json.bind.config.PropertyNamingStrategy;
+import javax.json.bind.config.PropertyOrderStrategy;
+import javax.json.bind.config.PropertyVisibilityStrategy;
+import javax.json.bind.serializer.JsonbDeserializer;
+import javax.json.bind.serializer.JsonbSerializer;
+import javax.json.spi.JsonProvider;
+import javax.json.stream.JsonGenerator;
+import javax.json.stream.JsonParserFactory;
import java.io.Closeable;
import java.io.IOException;
import java.lang.annotation.Annotation;
@@ -39,10 +89,12 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
@@ -65,60 +117,15 @@ import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;
-import javax.json.JsonBuilderFactory;
-import javax.json.JsonValue;
-import javax.json.bind.JsonbException;
-import javax.json.bind.adapter.JsonbAdapter;
-import javax.json.bind.annotation.JsonbCreator;
-import javax.json.bind.annotation.JsonbDateFormat;
-import javax.json.bind.annotation.JsonbNillable;
-import javax.json.bind.annotation.JsonbNumberFormat;
-import javax.json.bind.annotation.JsonbProperty;
-import javax.json.bind.annotation.JsonbPropertyOrder;
-import javax.json.bind.annotation.JsonbTransient;
-import javax.json.bind.annotation.JsonbTypeAdapter;
-import javax.json.bind.annotation.JsonbTypeDeserializer;
-import javax.json.bind.annotation.JsonbTypeSerializer;
-import javax.json.bind.config.PropertyNamingStrategy;
-import javax.json.bind.config.PropertyOrderStrategy;
-import javax.json.bind.config.PropertyVisibilityStrategy;
-import javax.json.bind.serializer.JsonbDeserializer;
-import javax.json.bind.serializer.JsonbSerializer;
-import javax.json.spi.JsonProvider;
-import javax.json.stream.JsonGenerator;
-import javax.json.stream.JsonParserFactory;
-
-import org.apache.johnzon.core.Types;
-import org.apache.johnzon.jsonb.converter.JohnzonJsonbAdapter;
-import org.apache.johnzon.jsonb.converter.JsonbDateConverter;
-import org.apache.johnzon.jsonb.converter.JsonbLocalDateConverter;
-import org.apache.johnzon.jsonb.converter.JsonbLocalDateTimeConverter;
-import org.apache.johnzon.jsonb.converter.JsonbNumberConverter;
-import org.apache.johnzon.jsonb.converter.JsonbValueConverter;
-import org.apache.johnzon.jsonb.converter.JsonbZonedDateTimeConverter;
-import org.apache.johnzon.jsonb.order.PerHierarchyAndLexicographicalOrderFieldComparator;
-import org.apache.johnzon.jsonb.reflect.GenericArrayTypeImpl;
-import org.apache.johnzon.jsonb.serializer.JohnzonDeserializationContext;
-import org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext;
-import org.apache.johnzon.jsonb.spi.JohnzonAdapterFactory;
-import org.apache.johnzon.mapper.Adapter;
-import org.apache.johnzon.mapper.Converter;
-import org.apache.johnzon.mapper.JohnzonAny;
-import org.apache.johnzon.mapper.JohnzonConverter;
-import org.apache.johnzon.mapper.MapperConverter;
-import org.apache.johnzon.mapper.MappingGenerator;
-import org.apache.johnzon.mapper.MappingParser;
-import org.apache.johnzon.mapper.ObjectConverter;
-import org.apache.johnzon.mapper.TypeAwareAdapter;
-import org.apache.johnzon.mapper.access.AccessMode;
-import org.apache.johnzon.mapper.access.BaseAccessMode;
-import org.apache.johnzon.mapper.access.FieldAccessMode;
-import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
-import org.apache.johnzon.mapper.access.Meta;
-import org.apache.johnzon.mapper.access.MethodAccessMode;
-import org.apache.johnzon.mapper.converter.ReversedAdapter;
-import org.apache.johnzon.mapper.internal.AdapterKey;
-import org.apache.johnzon.mapper.internal.ConverterAdapter;
+import static java.util.Arrays.asList;
+import static java.util.Comparator.comparing;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.johnzon.mapper.reflection.Converters.matches;
+import static org.apache.johnzon.mapper.reflection.Records.isRecord;
public class JsonbAccessMode implements AccessMode, Closeable {
private final PropertyNamingStrategy naming;
@@ -210,6 +217,10 @@ public class JsonbAccessMode implements AccessMode, Closeable {
}
factory = m;
}
+ final boolean record = isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null;
+ if (constructor == null && record) {
+ constructor = findRecordConstructor(clazz).orElse(null);
+ }
if (constructor == null && factory == null) {
invalidConstructorForDeserialization = Stream.of(clazz.getDeclaredConstructors())
.anyMatch(it -> it.getParameterCount() == 0 &&
@@ -223,7 +234,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
throw new JsonbException("Missing @JsonbCreator argument");
}
} :
- args -> {};
+ args -> {
+ };
final Type[] types;
final String[] params;
final Adapter<?, ?>[] converters;
@@ -239,7 +251,13 @@ public class JsonbAccessMode implements AccessMode, Closeable {
int i = 0;
for (final Parameter parameter : (finalConstructor == null ? finalFactory : finalConstructor).getParameters()) {
final JsonbProperty property = getAnnotation(parameter, JsonbProperty.class);
- params[i] = property != null ? property.value() : parameter.getName();
+ params[i] = property != null ?
+ property.value() :
+ (record ?
+ ofNullable(parameter.getAnnotation(JohnzonRecord.Name.class))
+ .map(JohnzonRecord.Name::value)
+ .orElseGet(parameter::getName) :
+ parameter.getName());
final JsonbTypeAdapter adapter = getAnnotation(parameter, JsonbTypeAdapter.class);
final JsonbDateFormat dateFormat = getAnnotation(parameter, JsonbDateFormat.class);
@@ -252,7 +270,7 @@ public class JsonbAccessMode implements AccessMode, Closeable {
validateAnnotations(parameter, adapter, dateFormat, numberFormat, johnzonConverter);
try {
- if (adapter != null) {
+ if (adapter != null || dateFormat != null || numberFormat != null) {
final Adapter converter = toConverter(
this.types, parameter.getType(), adapter, dateFormat, numberFormat);
if (matches(parameter.getParameterizedType(), converter)) {
@@ -282,10 +300,11 @@ public class JsonbAccessMode implements AccessMode, Closeable {
if (constructor == null && factory == null && !invalidConstructorForDeserialization) {
final Stream<Function<AnnotatedElement, String>> jsonbFn = Stream.of(this::getJsonbProperty);
- return delegate.findFactory(
+ final Factory delegateFactory = delegate.findFactory(
clazz,
(parameterNameExtractors == null ?
- jsonbFn : Stream.concat(jsonbFn, Stream.of(parameterNameExtractors))).toArray(Function[]::new));
+ jsonbFn : Stream.concat(jsonbFn, Stream.of(parameterNameExtractors))).toArray(Function[]::new));
+ return delegateFactory;
}
if (constructor != null || invalidConstructorForDeserialization) {
return constructorFactory(finalConstructor, invalidConstructorForDeserialization ? (Consumer<Object[]>) objects -> {
@@ -295,6 +314,17 @@ public class JsonbAccessMode implements AccessMode, Closeable {
return methodFactory(clazz, finalFactory, factoryValidator, types, params, converters, itemConverters, objectConverters);
}
+ private Optional<Constructor<?>> findRecordConstructor(final Class<?> clazz) {
+ return Stream.of(clazz.getDeclaredConstructors())
+ .max(comparing(Constructor::getParameterCount))
+ .map(c -> {
+ if (!c.isAccessible()) {
+ c.setAccessible(true);
+ }
+ return c;
+ });
+ }
+
private String getJsonbProperty(final AnnotatedElement a) {
final JsonbProperty p = Meta.getAnnotation(a, JsonbProperty.class);
return p != null ? p.value() : null;
@@ -448,6 +478,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
converter = new ConverterAdapter<>(new JsonbLocalDateConverter(dateFormat), LocalDate.class);
} else if (ZonedDateTime.class == type) {
converter = new ConverterAdapter<>(new JsonbZonedDateTimeConverter(dateFormat), ZonedDateTime.class);
+ } else if (OffsetDateTime.class == type) {
+ converter = new ConverterAdapter<>(new JsonbOffsetDateTimeConverter(dateFormat), OffsetDateTime.class);
} else { // can happen if set on the class, todo: refine the checks
converter = null; // todo: should we fallback on numberformat?
}
@@ -467,15 +499,54 @@ public class JsonbAccessMode implements AccessMode, Closeable {
public Map<String, Reader> findReaders(final Class<?> clazz) {
final Map<String, Reader> readers = delegate.findReaders(clazz);
+ final boolean record = isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null;
+ final Map<String, Parameter> recordParams = record ?
+ findRecordConstructor(clazz)
+ .map(c -> Stream.of(c.getParameters())
+ .collect(toMap(p -> ofNullable(p.getAnnotation(JohnzonRecord.Name.class))
+ .map(JohnzonRecord.Name::value)
+ .orElseGet(p::getName), identity())))
+ .orElseGet(Collections::emptyMap) :
+ null;
final Comparator<String> keyComparator = fieldComparator(clazz);
final Map<String, Reader> result = keyComparator == null ? new HashMap<>() : new TreeMap<>(keyComparator);
for (final Map.Entry<String, Reader> entry : readers.entrySet()) {
final Reader initialReader = entry.getValue();
+ final DecoratedType annotations = record ? new DecoratedType() {
+ private final Parameter parameter = recordParams.get(entry.getKey());
+
+ @Override
+ public Type getType() {
+ return initialReader.getType();
+ }
+
+ @Override
+ public <T extends Annotation> T getAnnotation(final Class<T> clazz) {
+ final T annotation = initialReader.getAnnotation(clazz);
+ return annotation == null && parameter != null ? parameter.getAnnotation(clazz) : annotation;
+ }
+
+ @Override
+ public <T extends Annotation> T getClassOrPackageAnnotation(final Class<T> clazz) {
+ final T annotation = parameter == null ? null : parameter.getAnnotation(clazz);
+ return annotation == null ? initialReader.getClassOrPackageAnnotation(clazz) : annotation;
+ }
+
+ @Override
+ public Adapter<?, ?> findConverter() {
+ return initialReader.findConverter();
+ }
+
+ @Override
+ public boolean isNillable(final boolean globalConfig) {
+ return initialReader.isNillable(globalConfig);
+ }
+ } : initialReader;
if (isTransient(initialReader, visibility)) {
validateAnnotationsOnTransientField(initialReader);
continue;
}
- if (initialReader.getAnnotation(JohnzonAny.class) != null) {
+ if (annotations.getAnnotation(JohnzonAny.class) != null) {
continue;
}
@@ -524,18 +595,18 @@ public class JsonbAccessMode implements AccessMode, Closeable {
final Object[] optionals = Object[].class.cast(finalReader.read(i));
return optionals == null ?
null : Stream.of(optionals)
- .map(Optional.class::cast)
- .map(o -> o.orElse(null))
- .toArray();
+ .map(Optional.class::cast)
+ .map(o -> o.orElse(null))
+ .toArray();
};
} else {
type = readerType;
reader = finalReader::read;
}
- final WriterConverters writerConverters = new WriterConverters(initialReader, types);
- final JsonbProperty property = initialReader.getAnnotation(JsonbProperty.class);
- final JsonbNillable nillable = initialReader.getClassOrPackageAnnotation(JsonbNillable.class);
+ final WriterConverters writerConverters = new WriterConverters(annotations, types);
+ final JsonbProperty property = annotations.getAnnotation(JsonbProperty.class);
+ final JsonbNillable nillable = annotations.getClassOrPackageAnnotation(JsonbNillable.class);
final boolean isNillable = isNillable(property, nillable);
final String key = property == null || property.value().isEmpty() ? naming.translateName(entry.getKey()) : property.value();
if (result.put(key, new Reader() {
@@ -642,8 +713,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
writer = (i, value) -> {
if (value != null) {
finalWriter.write(i, Stream.of(Object[].class.cast(value))
- .map(Optional::ofNullable)
- .toArray(Optional[]::new));
+ .map(Optional::ofNullable)
+ .toArray(Optional[]::new));
}
};
} else {
@@ -960,8 +1031,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
Set.class.isAssignableFrom(
types.asClass(parameterizedType.getRawType())) ? toSet() : toList();
fn = (json, mp) -> json.asJsonArray().stream()
- .map(i -> mapItem(i, paramType, mp, jsonbDeserializer))
- .collect(collector);
+ .map(i -> mapItem(i, paramType, mp, jsonbDeserializer))
+ .collect(collector);
}
}
}
@@ -1045,7 +1116,7 @@ public class JsonbAccessMode implements AccessMode, Closeable {
try {
MapperConverter mapperConverter = johnzonConverter.value().newInstance();
if (mapperConverter instanceof Converter) {
- converter = new ConverterAdapter<>((Converter) mapperConverter, reader.getType()) ;
+ converter = new ConverterAdapter<>((Converter) mapperConverter, reader.getType());
} else if (mapperConverter instanceof ObjectConverter.Writer) {
writer = (ObjectConverter.Writer) mapperConverter;
}
@@ -1074,8 +1145,8 @@ public class JsonbAccessMode implements AccessMode, Closeable {
private boolean hasRawType(final Type type) {
return Class.class.isInstance(type) ||
- (ParameterizedType.class.isInstance(type) &&
- Class.class.isInstance(ParameterizedType.class.cast(type).getRawType()));
+ (ParameterizedType.class.isInstance(type) &&
+ Class.class.isInstance(ParameterizedType.class.cast(type).getRawType()));
}
private Class<?> getRawType(final Type type) { // only intended to be used after hasRawType check
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java
new file mode 100644
index 0000000..81e28e3
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbRecordTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonb;
+
+import org.apache.johnzon.jsonb.test.JsonbRule;
+import org.apache.johnzon.mapper.JohnzonRecord;
+import org.junit.Rule;
+import org.junit.Test;
+
+import javax.json.bind.annotation.JsonbProperty;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+
+public class JsonbRecordTest {
+ @Rule
+ public final JsonbRule jsonb = new JsonbRule();
+
+ @Test
+ public void roundTrip() {
+ final Record ref = new Record(119, "Santa");
+ final String expectedJson = "{\"_name\":\"Santa\",\"age\":119}";
+ assertEquals(expectedJson, jsonb.toJson(ref));
+ assertEquals(ref, jsonb.fromJson(expectedJson, Record.class));
+ }
+
+ @JohnzonRecord
+ public static class Record {
+ private final int age;
+ private final String name;
+
+ public Record(@JohnzonRecord.Name("age") final int age,
+ @JohnzonRecord.Name("name") @JsonbProperty("_name") final String name) { // simulate custom constructor
+ this.age = age;
+ this.name = name;
+ }
+
+ public int age() {
+ return age;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return "Record{" +
+ "age=" + age +
+ ", name='" + name + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final Record record = Record.class.cast(o);
+ return age == record.age && Objects.equals(name, record.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(age, name);
+ }
+ }
+}