This is an automated email from the ASF dual-hosted git repository. struberg pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/johnzon.git
commit 46a1bb8103e9f6dc0cd6b174b5a85a22a578e499 Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Sun Apr 10 20:06:50 2022 +0200 fix DefaultPropertyVisibilityStrategy to respect the spec, ensure we clean the cache + fix backward compatibility of the FieldAndMethodAccessMode --- .../jsonb/DefaultPropertyVisibilityStrategy.java | 96 +++++++++----- .../org/apache/johnzon/jsonb/JohnzonBuilder.java | 2 +- .../org/apache/johnzon/jsonb/JsonbAccessMode.java | 45 +++++-- .../DefaultPropertyVisibilityStrategyTest.java | 45 ++++++- .../jsonb/api/experimental/JsonbExtensionTest.java | 4 - .../java/org/apache/johnzon/mapper/Cleanable.java | 23 ++++ .../org/apache/johnzon/mapper/MapperBuilder.java | 2 +- .../java/org/apache/johnzon/mapper/Mappings.java | 92 +++++++------- .../mapper/access/FieldAndMethodAccessMode.java | 42 ++++++- .../access/FieldAndMethodAccessModeTest.java | 138 +++++++++++++++++++++ 10 files changed, 393 insertions(+), 96 deletions(-) diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategy.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategy.java index 141b5a0..fe4d449 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategy.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategy.java @@ -18,64 +18,96 @@ */ package org.apache.johnzon.jsonb; -import static java.util.Optional.ofNullable; +import org.apache.johnzon.mapper.Cleanable; -import java.beans.Introspector; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbVisibility; +import javax.json.bind.config.PropertyVisibilityStrategy; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import javax.json.bind.annotation.JsonbProperty; -import javax.json.bind.annotation.JsonbVisibility; -import javax.json.bind.config.PropertyVisibilityStrategy; +import static java.util.Optional.ofNullable; -class DefaultPropertyVisibilityStrategy implements javax.json.bind.config.PropertyVisibilityStrategy { +class DefaultPropertyVisibilityStrategy implements javax.json.bind.config.PropertyVisibilityStrategy, Cleanable<Class<?>> { private final ConcurrentMap<Class<?>, PropertyVisibilityStrategy> strategies = new ConcurrentHashMap<>(); - private final ConcurrentMap<Class<?>, List<String>> getters = new ConcurrentHashMap<>(); + private final ConcurrentMap<Class<?>, Map<String, Boolean>> getters = new ConcurrentHashMap<>(); + private final ConcurrentMap<Class<?>, Map<String, Boolean>> setters = new ConcurrentHashMap<>(); private volatile boolean skipGetpackage; @Override public boolean isVisible(final Field field) { + return isVisible(field, field.getDeclaringClass(), true); + } + + public boolean isVisible(final Field field, final Class<?> root, final boolean useGetter) { if (field.getAnnotation(JsonbProperty.class) != null) { return true; } - final PropertyVisibilityStrategy strategy = strategies.computeIfAbsent( - field.getDeclaringClass(), this::visibilityStrategy); - return strategy == this ? isFieldVisible(field) : strategy.isVisible(field); + final PropertyVisibilityStrategy strategy = strategies.computeIfAbsent(root, this::visibilityStrategy); + return strategy == this ? isFieldVisible(field, root, useGetter) : strategy.isVisible(field); } - private boolean isFieldVisible(Field field) { + private boolean isFieldVisible(final Field field, final Class<?> root, final boolean useGetter) { if (!Modifier.isPublic(field.getModifiers())) { return false; } - // also check if there is any setter, in which case the field should be treated as non-visible as well. - return !getters.computeIfAbsent(field.getDeclaringClass(), this::calculateGetters).contains(field.getName()); + // 3.7.1. Scope and Field access strategy + // note: we should bind the class since a field of a parent class can have a getter in a child + if (!useGetter) { + return setters.computeIfAbsent(root, this::calculateSetters).getOrDefault(field.getName(), true); + } + return getters.computeIfAbsent(root, this::calculateGetters).getOrDefault(field.getName(), true); } /** * Calculate all the getters of the given class. */ - private List<String> calculateGetters(Class<?> clazz) { - List<String> getters = new ArrayList<>(); - for (Method m : clazz.getDeclaredMethods()) { - if (m.getParameterCount() == 0) { - if (m.getName().startsWith("get")) { - getters.add(Introspector.decapitalize(m.getName().substring(3))); - } else if (m.getName().startsWith("is")) { - getters.add(Introspector.decapitalize(m.getName().substring(2))); - } + private Map<String, Boolean> calculateGetters(final Class<?> clazz) { + final Map<String, Boolean> getters = new HashMap<>(); + for (final Method m : clazz.getDeclaredMethods()) { + if (m.getParameterCount() > 0) { + continue; + } + if (m.getName().startsWith("get") && m.getName().length() > 3) { + getters.put( + Character.toLowerCase(m.getName().charAt(3)) + m.getName().substring(4), + Modifier.isPublic(m.getModifiers())); + } else if (m.getName().startsWith("is") && m.getName().length() > 2) { + getters.put( + Character.toLowerCase(m.getName().charAt(2)) + m.getName().substring(3), + Modifier.isPublic(m.getModifiers())); + } + } + final Class<?> superclass = clazz.getSuperclass(); + if (superclass != Object.class && superclass != null && !"java.lang.Record".equals(superclass.getName())) { + calculateGetters(superclass).forEach(getters::putIfAbsent); // don't override child getter if exists + } + return getters.isEmpty() ? Collections.emptyMap() : getters; + } + + private Map<String, Boolean> calculateSetters(final Class<?> clazz) { + final Map<String, Boolean> result = new HashMap<>(); + for (final Method m : clazz.getDeclaredMethods()) { + if (m.getParameterCount() != 1) { + continue; + } + if (m.getName().startsWith("set") && m.getName().length() > 3) { + result.put( + Character.toLowerCase(m.getName().charAt(3)) + m.getName().substring(4), + Modifier.isPublic(m.getModifiers())); } } if (clazz.getSuperclass() != Object.class) { - getters.addAll(calculateGetters(clazz.getSuperclass())); + calculateSetters(clazz.getSuperclass()).forEach(result::putIfAbsent); } - return getters.isEmpty() ? Collections.emptyList() : getters; + return result.isEmpty() ? Collections.emptyMap() : result; } @Override @@ -120,8 +152,9 @@ class DefaultPropertyVisibilityStrategy implements javax.json.bind.config.Proper } if (p == null) { try { - p = ofNullable(type.getClassLoader()).orElseGet(ClassLoader::getSystemClassLoader) - .loadClass(parentPack + ".package-info").getPackage(); + p = ofNullable(type.getClassLoader()) + .orElseGet(ClassLoader::getSystemClassLoader) + .loadClass(parentPack + ".package-info").getPackage(); } catch (final ClassNotFoundException e) { // no-op } @@ -129,4 +162,11 @@ class DefaultPropertyVisibilityStrategy implements javax.json.bind.config.Proper } return this; } + + @Override + public void clean(final Class<?> clazz) { + getters.remove(clazz); + setters.remove(clazz); + strategies.remove(clazz); + } } diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java index b304bd1..c068809 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java @@ -221,7 +221,7 @@ public class JohnzonBuilder implements JsonbBuilder { factory, jsonp, builderFactorySupplier, parserFactoryProvider, config.getProperty("johnzon.accessModeDelegate") .map(this::toAccessMode) - .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false)), + .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false, false, true)), // this changes in v3 of the spec so let's use this behavior which makes everyone happy by default config.getProperty("johnzon.failOnMissingCreatorValues") .map(this::toBool) 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 4bf1911..fbcb4a8 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 @@ -33,6 +33,7 @@ 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.Cleanable; import org.apache.johnzon.mapper.Converter; import org.apache.johnzon.mapper.JohnzonAny; import org.apache.johnzon.mapper.JohnzonConverter; @@ -127,7 +128,7 @@ 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 { +public class JsonbAccessMode implements AccessMode, Closeable, Cleanable<Class<?>> { private final PropertyNamingStrategy naming; private final String order; private final PropertyVisibilityStrategy visibility; @@ -542,7 +543,7 @@ public class JsonbAccessMode implements AccessMode, Closeable { return initialReader.isNillable(globalConfig); } } : initialReader; - if (isTransient(initialReader, visibility)) { + if (isTransient(initialReader, visibility, clazz, true)) { validateAnnotationsOnTransientField(initialReader); continue; } @@ -670,7 +671,7 @@ public class JsonbAccessMode implements AccessMode, Closeable { final Map<String, Writer> result = keyComparator == null ? new HashMap<>() : new TreeMap<>(keyComparator); for (final Map.Entry<String, Writer> entry : writers.entrySet()) { Writer initialWriter = entry.getValue(); - if (isTransient(initialWriter, visibility)) { + if (isTransient(initialWriter, visibility, clazz, false)) { validateAnnotationsOnTransientField(initialWriter); continue; } @@ -860,17 +861,17 @@ public class JsonbAccessMode implements AccessMode, Closeable { isOptional(GenericArrayType.class.cast(value.getType()).getGenericComponentType()); } - private boolean isTransient(final DecoratedType dt, final PropertyVisibilityStrategy visibility) { + private boolean isTransient(final DecoratedType dt, final PropertyVisibilityStrategy visibility, final Class<?> root, final boolean read) { if (!FieldAndMethodAccessMode.CompositeDecoratedType.class.isInstance(dt)) { - return isTransient(dt) || shouldSkip(visibility, dt); + return isTransient(dt) || shouldSkip(visibility, dt, root, read); } final FieldAndMethodAccessMode.CompositeDecoratedType cdt = FieldAndMethodAccessMode.CompositeDecoratedType.class.cast(dt); return isTransient(cdt.getType1()) || isTransient(cdt.getType2()) || - (shouldSkip(visibility, cdt.getType1()) && shouldSkip(visibility, cdt.getType2())); + (shouldSkip(visibility, cdt.getType1(), root, read) && shouldSkip(visibility, cdt.getType2(), root, read)); } - private boolean shouldSkip(final PropertyVisibilityStrategy visibility, final DecoratedType t) { - return isNotVisible(visibility, t); + private boolean shouldSkip(final PropertyVisibilityStrategy visibility, final DecoratedType t, final Class<?> root, final boolean read) { + return isNotVisible(visibility, t, root, read); } private boolean isTransient(final DecoratedType t) { @@ -885,11 +886,22 @@ public class JsonbAccessMode implements AccessMode, Closeable { return false; } - private boolean isNotVisible(PropertyVisibilityStrategy visibility, DecoratedType t) { - return !(FieldAccessMode.FieldDecoratedType.class.isInstance(t) ? - visibility.isVisible(FieldAccessMode.FieldDecoratedType.class.cast(t).getField()) - : (MethodAccessMode.MethodDecoratedType.class.isInstance(t) && - visibility.isVisible(MethodAccessMode.MethodDecoratedType.class.cast(t).getMethod()))); + private boolean isNotVisible(final PropertyVisibilityStrategy visibility, + final DecoratedType t, + final Class<?> root, + final boolean read) { + if (FieldAccessMode.FieldDecoratedType.class.isInstance(t)) { + final Field field = FieldAccessMode.FieldDecoratedType.class.cast(t).getField(); + if (DefaultPropertyVisibilityStrategy.class.isInstance(visibility)) { + return !DefaultPropertyVisibilityStrategy.class.cast(visibility).isVisible(field, root, read); + } + return !visibility.isVisible(field); + } + if (MethodAccessMode.MethodDecoratedType.class.isInstance(t)) { + final Method method = MethodAccessMode.MethodDecoratedType.class.cast(t).getMethod(); + return !visibility.isVisible(method); + } + return false; } private Comparator<String> orderComparator(final Class<?> clazz) { @@ -967,6 +979,13 @@ public class JsonbAccessMode implements AccessMode, Closeable { return Meta.findMeta(param.getAnnotations(), api); } + @Override + public void clean(final Class<?> value) { + if (Cleanable.class.isInstance(visibility)) { + Cleanable.class.cast(visibility).clean(value); + } + } + private class ReaderConverters { private Adapter<?, ?> converter; private ObjectConverter.Reader reader; diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategyTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategyTest.java index 17eeab2..b780497 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategyTest.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DefaultPropertyVisibilityStrategyTest.java @@ -18,6 +18,7 @@ */ package org.apache.johnzon.jsonb; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.lang.reflect.Field; @@ -26,6 +27,7 @@ import java.lang.reflect.Method; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTransient; import javax.json.bind.annotation.JsonbVisibility; import org.junit.Test; @@ -47,6 +49,26 @@ public class DefaultPropertyVisibilityStrategyTest { } } + @Test + public void hiddenGetter() throws Exception { + try (final Jsonb jsonb = JsonbBuilder.create()) { + assertFalse(jsonb.fromJson("{\"foo\":true}", HideAllModel.class).isFoo()); + assertFalse(jsonb.fromJson("{\"foo\":true}", HideAllDefaultModel.class).isFoo()); + } + } + + public static class HideAll extends DefaultPropertyVisibilityStrategy { + @Override + public boolean isVisible(final Field field) { + return false; + } + + @Override + public boolean isVisible(final Method method) { + return false; + } + } + public static class MyVisibility extends DefaultPropertyVisibilityStrategy { @Override public boolean isVisible(final Field field) { @@ -64,7 +86,7 @@ public class DefaultPropertyVisibilityStrategyTest { @JsonbProperty private boolean foo; - public final boolean isFoo() { + public boolean isFoo() { return foo; } } @@ -73,7 +95,26 @@ public class DefaultPropertyVisibilityStrategyTest { @JsonbProperty private boolean foo; - public final boolean isFoo() { + public boolean isFoo() { + return foo; + } + } + + @JsonbVisibility(HideAll.class) + public static final class HideAllModel { + protected boolean foo; + + @JsonbTransient + public boolean isFoo() { + return foo; + } + } + + public static final class HideAllDefaultModel { + protected boolean foo; + + @JsonbTransient + public boolean isFoo() { return foo; } } diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/api/experimental/JsonbExtensionTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/api/experimental/JsonbExtensionTest.java index fc8eacd..a2d612c 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/api/experimental/JsonbExtensionTest.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/api/experimental/JsonbExtensionTest.java @@ -218,10 +218,6 @@ public class JsonbExtensionTest { return value; } - public void setValue(Object value) { - this.value = value; - } - @Override public boolean equals(final Object obj) { // for test return Wrapper.class.isInstance(obj) && diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Cleanable.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Cleanable.java new file mode 100644 index 0000000..e90a363 --- /dev/null +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Cleanable.java @@ -0,0 +1,23 @@ +/* + * 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.mapper; + +public interface Cleanable<A> { + void clean(A value); +} diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java index b02455f..5075ed4 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java @@ -169,7 +169,7 @@ public class MapperBuilder { } else if ("strict-method".equalsIgnoreCase(accessModeName)) { accessMode = new MethodAccessMode(supportConstructors, supportHiddenAccess, false); } else if ("both".equalsIgnoreCase(accessModeName) || accessModeName == null) { - accessMode = new FieldAndMethodAccessMode(supportConstructors, supportHiddenAccess, useGetterForCollections); + accessMode = new FieldAndMethodAccessMode(supportConstructors, supportHiddenAccess, useGetterForCollections, true, false); } else { throw new IllegalArgumentException("Unsupported access mode: " + accessModeName); } 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 686f285..7c1776e 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 @@ -470,59 +470,65 @@ public class Mappings { final Map<String, Getter> getters = fieldComparator == null ? newOrderedMap(Getter.class) : new TreeMap<>(fieldComparator); final Map<String, Setter> setters = fieldComparator == null ? newOrderedMap(Setter.class) : new TreeMap<>(fieldComparator); - final Map<String, AccessMode.Reader> readers = accessMode.findReaders(clazz); - final Map<String, AccessMode.Writer> writers = accessMode.findWriters(clazz); - - final Collection<String> virtualFields = new HashSet<String>(); - { - final JohnzonVirtualObjects virtualObjects = clazz.getAnnotation(JohnzonVirtualObjects.class); - if (virtualObjects != null) { - for (final JohnzonVirtualObject virtualObject : virtualObjects.value()) { + try { + final Map<String, AccessMode.Reader> readers = accessMode.findReaders(clazz); + final Map<String, AccessMode.Writer> writers = accessMode.findWriters(clazz); + + final Collection<String> virtualFields = new HashSet<String>(); + { + final JohnzonVirtualObjects virtualObjects = clazz.getAnnotation(JohnzonVirtualObjects.class); + if (virtualObjects != null) { + for (final JohnzonVirtualObject virtualObject : virtualObjects.value()) { + handleVirtualObject(virtualFields, virtualObject, getters, setters, readers, writers, copyDate, clazz); + } + } + + final JohnzonVirtualObject virtualObject = clazz.getAnnotation(JohnzonVirtualObject.class); + if (virtualObject != null) { handleVirtualObject(virtualFields, virtualObject, getters, setters, readers, writers, copyDate, clazz); } } - final JohnzonVirtualObject virtualObject = clazz.getAnnotation(JohnzonVirtualObject.class); - if (virtualObject != null) { - handleVirtualObject(virtualFields, virtualObject, getters, setters, readers, writers, copyDate, clazz); + for (final Map.Entry<String, AccessMode.Reader> reader : readers.entrySet()) { + final String key = reader.getKey(); + if (virtualFields.contains(key)) { + continue; + } + addGetterIfNeeded(getters, key, reader.getValue(), copyDate, resolvedTypes); } - } - for (final Map.Entry<String, AccessMode.Reader> reader : readers.entrySet()) { - final String key = reader.getKey(); - if (virtualFields.contains(key)) { - continue; + for (final Map.Entry<String, AccessMode.Writer> writer : writers.entrySet()) { + final String key = writer.getKey(); + if (virtualFields.contains(key)) { + continue; + } + addSetterIfNeeded(setters, key, writer.getValue(), copyDate, clazz, resolvedTypes); } - addGetterIfNeeded(getters, key, reader.getValue(), copyDate, resolvedTypes); - } - for (final Map.Entry<String, AccessMode.Writer> writer : writers.entrySet()) { - final String key = writer.getKey(); - if (virtualFields.contains(key)) { - continue; + 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), + accessMode.findReader(clazz), + accessMode.findWriter(clazz), + anyGetter != null ? new Getter( + new MethodAccessMode.MethodReader(anyGetter, anyGetter.getReturnType()), + 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); + + return mapping; + } finally { + if (Cleanable.class.isInstance(accessMode)) { + ((Cleanable<Class<?>>) accessMode).clean(clazz); } - addSetterIfNeeded(setters, key, writer.getValue(), copyDate, clazz, resolvedTypes); } - - 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), - accessMode.findReader(clazz), - accessMode.findWriter(clazz), - anyGetter != null ? new Getter( - new MethodAccessMode.MethodReader(anyGetter, anyGetter.getReturnType()), - 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); - - return mapping; } protected Class<?> findModelClass(final Class<?> inClazz) { diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java index a651d6d..c06de48 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java @@ -29,6 +29,7 @@ import java.beans.Introspector; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashMap; @@ -38,12 +39,31 @@ import java.util.Map; public class FieldAndMethodAccessMode extends BaseAccessMode { private final FieldAccessMode fields; private final MethodAccessMode methods; + private final boolean alwaysPreferMethodVisibility; + private final boolean ignoreVisibilityFilter; public FieldAndMethodAccessMode(final boolean useConstructor, final boolean acceptHiddenConstructor, - final boolean useGettersAsWriter) { + final boolean useGettersAsWriter, final boolean alwaysPreferMethodVisibility, + final boolean ignoreVisibilityFilter) { super(useConstructor, acceptHiddenConstructor); this.fields = new FieldAccessMode(useConstructor, acceptHiddenConstructor); - this.methods = new MethodAccessMode(useConstructor, acceptHiddenConstructor, useGettersAsWriter); + this.methods = new MethodAccessMode( + useConstructor, acceptHiddenConstructor, useGettersAsWriter); + this.alwaysPreferMethodVisibility = alwaysPreferMethodVisibility; + this.ignoreVisibilityFilter = ignoreVisibilityFilter; + } + + @Deprecated // backward compat + public FieldAndMethodAccessMode(final boolean useConstructor, final boolean acceptHiddenConstructor, + final boolean useGettersAsWriter, final boolean alwaysPreferMethodVisibility) { + this(useConstructor, acceptHiddenConstructor, useGettersAsWriter, alwaysPreferMethodVisibility, false); + } + + // backward compatibility, don't delete since it can be used from user code in jsonb delegate access mode property + @Deprecated + public FieldAndMethodAccessMode(final boolean useConstructor, final boolean acceptHiddenConstructor, + final boolean useGettersAsWriter) { + this(useConstructor, acceptHiddenConstructor, useGettersAsWriter, true, false); } @@ -67,7 +87,7 @@ public class FieldAndMethodAccessMode extends BaseAccessMode { m = getMethod("is" + Character.toUpperCase(key.charAt(0)) + (key.length() > 1 ? key.substring(1) : ""), clazz); } boolean skip = false; - if (m != null) { + if (m != null && (ignoreVisibilityFilter || Modifier.isPublic(m.getModifiers()))) { for (final Reader w : methodReaders.values()) { if (MethodAccessMode.MethodDecoratedType.class.cast(w).getMethod().equals(m)) { if (w.getAnnotation(JohnzonProperty.class) != null || w.getAnnotation(JohnzonIgnore.class) != null) { @@ -76,6 +96,8 @@ public class FieldAndMethodAccessMode extends BaseAccessMode { break; } } + } else if (!ignoreVisibilityFilter && m != null) { + continue; } if (skip) { continue; @@ -123,8 +145,18 @@ public class FieldAndMethodAccessMode extends BaseAccessMode { private Method getMethod(final String methodName, final Class<?> type, final Class<?>... args) { try { + if (alwaysPreferMethodVisibility) { + return type.getDeclaredMethod(methodName, args); + } return type.getMethod(methodName, args); } catch (final NoSuchMethodException e) { + if (alwaysPreferMethodVisibility) { + try { + return type.getMethod(methodName, args); + } catch (final NoSuchMethodException e2) { + // no-op + } + } return null; } } @@ -153,7 +185,7 @@ public class FieldAndMethodAccessMode extends BaseAccessMode { final String key = entry.getKey(); final Method m = getMethod("set" + Character.toUpperCase(key.charAt(0)) + (key.length() > 1 ? key.substring(1) : ""), clazz, toType(entry.getValue().getType())); boolean skip = false; - if (m != null) { + if (m != null && (ignoreVisibilityFilter || Modifier.isPublic(m.getModifiers()))) { for (final Writer w : metodWriters.values()) { if (MethodAccessMode.MethodDecoratedType.class.cast(w).getMethod().equals(m)) { if (w.getAnnotation(JohnzonProperty.class) != null) { @@ -162,6 +194,8 @@ public class FieldAndMethodAccessMode extends BaseAccessMode { break; } } + } else if (!ignoreVisibilityFilter && m != null) { + continue; } if (skip) { continue; diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessModeTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessModeTest.java new file mode 100644 index 0000000..4c2e948 --- /dev/null +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessModeTest.java @@ -0,0 +1,138 @@ +/* + * 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.mapper.access; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class FieldAndMethodAccessModeTest { + @Parameterized.Parameters + public static Collection<Object[]> data() { + return asList( + new Object[]{ + new FieldAndMethodAccessMode(true, true, false, true, false), + POJO.class, + singletonList("foo"), + singletonList("foo") + }, + new Object[]{ + new FieldAndMethodAccessMode(true, true, false, true, false), + POJOProtectedSetter.class, + singletonList("foo"), + emptyList() + }, + new Object[]{ + new FieldAndMethodAccessMode(true, true, false, true, false), + POJOProtectedGetter.class, + emptyList(), + singletonList("foo") + }, + new Object[]{ + new FieldAndMethodAccessMode(true, true, false, true, true), + POJO.class, + singletonList("foo"), + singletonList("foo") + }, + new Object[]{ + new FieldAndMethodAccessMode(true, true, false, true, true), + POJOProtectedSetter.class, + singletonList("foo"), + singletonList("foo") + }, + new Object[]{ + new FieldAndMethodAccessMode(true, true, false, true, true), + POJOProtectedGetter.class, + singletonList("foo"), + singletonList("foo") + } + ); + } + + private final FieldAndMethodAccessMode accessMode; + private final Class<?> model; + private final Collection<String> resultGetters; + private final Collection<String> resultSetters; + + public FieldAndMethodAccessModeTest(final FieldAndMethodAccessMode accessMode, + final Class<?> model, + final List<String> resultGetters, + final List<String> resultSetters) { + this.accessMode = accessMode; + this.model = model; + this.resultGetters = new HashSet<>(resultGetters); + this.resultSetters = new HashSet<>(resultSetters); + } + + @Test + public void getters() { + assertEquals(resultGetters, accessMode.findReaders(model).keySet()); + } + + @Test + public void setters() { + assertEquals(resultSetters, accessMode.findWriters(model).keySet()); + } + + public static class POJO { + private int foo; + + public int getFoo() { + return foo; + } + + public void setFoo(final int foo) { + this.foo = foo; + } + } + + public static class POJOProtectedSetter { + private int foo; + + public int getFoo() { + return foo; + } + + protected void setFoo(final int foo) { + this.foo = foo; + } + } + + public static class POJOProtectedGetter { + private int foo; + + protected int getFoo() { + return foo; + } + + public void setFoo(final int foo) { + this.foo = foo; + } + } +}