JOHNZON-314 support @JohnzonAny on a field
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 3ab3fd0..1130f67 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
@@ -728,6 +728,11 @@
}
@Override
+ public Field findAnyField(final Class<?> clazz) {
+ return partialDelegate.findAnyField(clazz);
+ }
+
+ @Override
public void afterParsed(final Class<?> clazz) {
parsingCache.remove(clazz);
partialDelegate.afterParsed(clazz);
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/AnySupportTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/AnySupportTest.java
new file mode 100644
index 0000000..1cf040f
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/AnySupportTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.JohnzonAny;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Objects;
+
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.assertEquals;
+
+public class AnySupportTest {
+ @Rule
+ public final JsonbRule rule = new JsonbRule();
+
+ @Test
+ public void roundTrip() {
+ final Foo foo = rule.fromJson("{\"a\":\"b\"}", Foo.class);
+ assertEquals(singletonMap("a", "b"), foo.values);
+ assertEquals("{\"a\":\"b\"}", rule.toJson(foo));
+ }
+
+ @Test
+ public void subObject() {
+ final Bar object = rule.fromJson("{\"a\":{\"b\":\"c\"}}", Bar.class);
+ final Foo foo = new Foo();
+ foo.values = singletonMap("b", "c");
+ assertEquals(singletonMap("a", foo), object.values);
+ assertEquals("{\"a\":{\"b\":\"c\"}}", rule.toJson(object));
+ }
+
+ public static class Foo {
+ @JohnzonAny
+ private Map<String, String> values;
+
+ public Map<String, String> getValues() {
+ return values;
+ }
+
+ public void setValues(final Map<String, String> values) {
+ this.values = values;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Foo foo = (Foo) o;
+ return values.equals(foo.values);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(values);
+ }
+ }
+
+ public static class Bar {
+ @JohnzonAny
+ private Map<String, Foo> values;
+
+ public Map<String, Foo> getValues() {
+ return values;
+ }
+
+ public void setValues(final Map<String, Foo> values) {
+ this.values = values;
+ }
+ }
+}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonAny.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonAny.java
index 16338e3..f33a583 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonAny.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonAny.java
@@ -22,10 +22,11 @@
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
-@Target({METHOD, ANNOTATION_TYPE})
+@Target({METHOD, ANNOTATION_TYPE, FIELD})
public @interface JohnzonAny {
}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
index 26d3e12..1b7a3a9 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
@@ -78,6 +78,7 @@
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
import static javax.json.JsonValue.ValueType.ARRAY;
import static javax.json.JsonValue.ValueType.FALSE;
import static javax.json.JsonValue.ValueType.NULL;
@@ -424,7 +425,9 @@
final String key = entry.getKey();
if (!classMapping.setters.containsKey(key)) {
try {
- classMapping.anySetter.invoke(t, key, toValue(null, entry.getValue(), null, null, Object.class, null,
+ classMapping.anySetter.invoke(t, key,
+ toValue(null, entry.getValue(), null, null,
+ classMapping.anySetter.getGenericParameterTypes()[1], null,
isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, entry.getKey()) : null, type));
} catch (final IllegalAccessException e) {
throw new IllegalStateException(e);
@@ -433,6 +436,16 @@
}
}
}
+ } else if (classMapping.anyField != null) {
+ final Type tRef = type;
+ try {
+ classMapping.anyField.set(t, object.entrySet().stream()
+ .collect(toMap(Map.Entry::getKey, e -> toValue(null, e.getValue(), null, null,
+ ParameterizedType.class.cast(classMapping.anyField.getGenericType()).getActualTypeArguments()[1], null,
+ isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, e.getKey()) : null, tRef))));
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
}
if (classMapping.mapAdder != null) {
object.entrySet().stream()
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
index 6991c9d..0d8adbb 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
@@ -25,6 +25,7 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
+import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@@ -53,6 +54,7 @@
import java.util.concurrent.ConcurrentMap;
import org.apache.johnzon.mapper.access.AccessMode;
+import org.apache.johnzon.mapper.access.FieldAccessMode;
import org.apache.johnzon.mapper.access.MethodAccessMode;
import org.apache.johnzon.mapper.converter.DateWithCopyConverter;
import org.apache.johnzon.mapper.converter.EnumConverter;
@@ -72,6 +74,7 @@
public final ObjectConverter.Writer writer;
public final Getter anyGetter;
public final Method anySetter;
+ public final Field anyField;
public final Method mapAdder;
public final Class<?> mapAdderType;
@@ -83,7 +86,7 @@
final Map<String, Getter> getters, final Map<String, Setter> setters,
final Adapter<?, ?> adapter,
final ObjectConverter.Reader<?> reader, final ObjectConverter.Writer<?> writer,
- final Getter anyGetter, final Method anySetter,
+ final Getter anyGetter, final Method anySetter, final Field anyField,
final Method mapAdder) {
this.clazz = clazz;
this.factory = factory;
@@ -94,6 +97,7 @@
this.reader = reader;
this.anyGetter = anyGetter;
this.anySetter = anySetter;
+ this.anyField = anyField;
this.mapAdder = mapAdder;
this.mapAdderType = mapAdder == null ? null : mapAdder.getParameterTypes()[1];
}
@@ -479,6 +483,7 @@
}
final Method anyGetter = accessMode.findAnyGetter(clazz);
+ final Field anyField = accessMode.findAnyField(clazz);
final ClassMapping mapping = new ClassMapping(
clazz, accessMode.findFactory(clazz), getters, setters,
accessMode.findAdapter(clazz),
@@ -486,8 +491,11 @@
accessMode.findWriter(clazz),
anyGetter != null ? new Getter(
new MethodAccessMode.MethodReader(anyGetter, anyGetter.getReturnType()),
- false,false, false, false, true, null, null, -1, null) : null,
+ false,false, false, false, true, null, null, -1, null) :
+ (anyField != null ? new Getter(new FieldAccessMode.FieldReader(anyField, anyField.getGenericType()),
+ false,false, false, false, true, null, null, -1, null) : null),
accessMode.findAnySetter(clazz),
+ anyField,
Map.class.isAssignableFrom(clazz) ? accessMode.findMapAdder(clazz) : null);
accessMode.afterParsed(clazz);
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/AccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/AccessMode.java
index 4f12fa2..dc7a462 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/AccessMode.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/AccessMode.java
@@ -20,6 +20,7 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -113,6 +114,7 @@
Adapter<?, ?> findAdapter(Class<?> clazz);
Method findAnyGetter(Class<?> clazz);
Method findAnySetter(Class<?> clazz);
+ Field findAnyField(Class<?> clazz);
default Method findMapAdder(final Class<?> clazz) {
return MapHelper.find((name, type, param) -> type.getMethod("add" + name, String.class, param), clazz);
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
index 1a5931e..edbc379 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
@@ -30,15 +30,18 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -302,7 +305,7 @@
for (final Method current : clazz.getMethods()) {
if (current.getAnnotation(JohnzonAny.class) != null) {
final Class<?>[] parameterTypes = current.getParameterTypes();
- if (parameterTypes.length == 2 && parameterTypes[0] == String.class && parameterTypes[1] == Object.class) {
+ if (parameterTypes.length == 2 && parameterTypes[0] == String.class) {
if (m != null) {
throw new IllegalArgumentException("Ambiguous @JohnzonAny on " + m + " and " + current);
}
@@ -313,6 +316,24 @@
return m;
}
+ @Override
+ public Field findAnyField(final Class<?> clazz) {
+ if (clazz.isInterface() || clazz.isEnum()) {
+ return null;
+ }
+ Class<?> current = clazz;
+ final Set<Class<?>> visited = new HashSet<>();
+ while (current != null && current != Object.class && visited.add(current)) {
+ for (final Field f : current.getDeclaredFields()) {
+ if (f.isAnnotationPresent(JohnzonAny.class)) { // todo: validation? waiting for jsonb standard behavior
+ return f;
+ }
+ }
+ current = clazz.getSuperclass();
+ }
+ return null;
+ }
+
private <T> Map<String, T> sanitize(final Class<?> type, final Map<String, T> delegate) {
for (final String field : fieldFilteringStrategy.select(type)) {
delegate.remove(field);