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 <rmannibu...@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); + } + } +}