diff --git a/johnzon-jsonb/pom.xml b/johnzon-jsonb/pom.xml index 468d51e9..9f702d7a 100644 --- a/johnzon-jsonb/pom.xml +++ b/johnzon-jsonb/pom.xml @@ -230,6 +230,10 @@ **/AnnotationTypeInfoTest + + en_US.UTF-8 + + ${project.build.directory}/jimage ${project.build.directory}/signaturedirectory/jakarta.json.bind-api.jar:${project.build.directory}/jimage/java.base:${project.build.directory}/jimage/java.rmi:${project.build.directory}/jimage/java.sql:${project.build.directory}/jimage/java.naming 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 460d41c8..56dcf11a 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 @@ -36,6 +36,7 @@ import org.apache.johnzon.mapper.SerializeValueFilter; import org.apache.johnzon.mapper.access.AccessMode; import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode; +import org.apache.johnzon.mapper.access.MethodAccessMode; import org.apache.johnzon.mapper.converter.LocaleConverter; import org.apache.johnzon.mapper.internal.AdapterKey; @@ -209,24 +210,10 @@ public Jsonb build() { final AccessMode accessMode = config.getProperty("johnzon.accessMode") .map(this::toAccessMode) - .orElseGet(() -> new JsonbAccessMode( + .orElseGet(() -> newJsonbAccessMode( propertyNamingStrategy, orderValue, visibilityStrategy, - !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), - builder.getAdapters(), - factory, jsonp, builderFactorySupplier, parserFactoryProvider, - config.getProperty("johnzon.accessModeDelegate") - .map(this::toAccessMode) - .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) - .orElseGet(() -> config.getProperty("jsonb.creator-parameters-required") - .map(this::toBool) - .orElse(false)), - isNillable, - config.getProperty("johnzon.supportsPrivateAccess") - .map(this::toBool) - .orElse(false))); + namingStrategyValue, factory, builderFactorySupplier, parserFactoryProvider, + isNillable)); builder.setAccessMode(accessMode); config.getProperty("johnzon.snippetMaxLength") @@ -235,6 +222,12 @@ public Jsonb build() { Integer.parseInt(it.toString())) .ifPresent(builder::setSnippetMaxLength); + config.getProperty("johnzon.version") + .map(it -> Number.class.isInstance(it)? + Number.class.cast(it).intValue() : + Integer.parseInt(it.toString())) + .ifPresent(builder::setVersion); + config.getProperty("johnzon.use-biginteger-stringadapter") .or(() -> Optional.ofNullable(System.getProperty("johnzon.use-biginteger-stringadapter"))) .map(Object::toString).map(Boolean::parseBoolean) @@ -381,6 +374,75 @@ private Integer toInt(final Object v) { return !Integer.class.isInstance(v) ? Integer.parseInt(v.toString()) : Integer.class.cast(v); } + private Set toExcludedMethods(final Object v) { + if (Set.class.isInstance(v)) { + return Set.class.cast(v); + } + final String str = v.toString().trim(); + if (str.isEmpty()) { + return Set.of(); + } + final Set result = new HashSet<>(); + for (final String s : str.split(",")) { + result.add(s.trim()); + } + return result; + } + + private JsonbAccessMode newJsonbAccessMode(final PropertyNamingStrategy propertyNamingStrategy, + final String orderValue, + final PropertyVisibilityStrategy visibilityStrategy, + final Optional namingStrategyValue, + final JohnzonAdapterFactory factory, + final Supplier builderFactorySupplier, + final Supplier parserFactoryProvider, + final boolean isNillable) { + final AccessMode delegateMode = config.getProperty("johnzon.accessModeDelegate") + .map(this::toAccessMode) + .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false, false, true)); + if (MethodAccessMode.class.isInstance(delegateMode)) { + ifExcludedMethods() + .ifPresent(MethodAccessMode.class.cast(delegateMode)::setExcludedMethods); + ifSupportAllRecordAttributes() + .ifPresent(v -> MethodAccessMode.class.cast(delegateMode).setSupportAllRecordAttributes(v.booleanValue())); + } else if (FieldAndMethodAccessMode.class.isInstance(delegateMode)) { + ifExcludedMethods() + .ifPresent(FieldAndMethodAccessMode.class.cast(delegateMode)::setExcludedMethods); + ifSupportAllRecordAttributes() + .ifPresent(v -> FieldAndMethodAccessMode.class.cast(delegateMode).setSupportAllRecordAttributes(v.booleanValue())); + } + return new JsonbAccessMode( + propertyNamingStrategy, orderValue, visibilityStrategy, + !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), + builder.getAdapters(), + factory, jsonp, builderFactorySupplier, parserFactoryProvider, + delegateMode, + config.getProperty("johnzon.failOnMissingCreatorValues") + .map(this::toBool) + .orElseGet(() -> config.getProperty("jsonb.creator-parameters-required") + .map(this::toBool) + .orElse(false)), + isNillable, + config.getProperty("johnzon.supportsPrivateAccess") + .map(this::toBool) + .orElse(false), + config.getProperty("johnzon.version") + .map(it -> Number.class.isInstance(it) ? + Number.class.cast(it).intValue() : + Integer.parseInt(it.toString())) + .orElse(-1)); + } + + private Optional ifSupportAllRecordAttributes() { + return config.getProperty("johnzon.accessMode.supportAllRecordAttributes") + .map(this::toBool); + } + + private Optional> ifExcludedMethods() { + return config.getProperty("johnzon.accessMode.excludedMethods") + .map(this::toExcludedMethods); + } + private AccessMode toAccessMode(final Object s) { if (String.class.isInstance(s)) { try { 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 ef0e7069..1f876375 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 @@ -37,6 +37,7 @@ import org.apache.johnzon.mapper.Converter; import org.apache.johnzon.mapper.JohnzonAny; import org.apache.johnzon.mapper.JohnzonConverter; +import org.apache.johnzon.mapper.JohnzonIgnore; import org.apache.johnzon.mapper.JohnzonRecord; import org.apache.johnzon.mapper.MapperConverter; import org.apache.johnzon.mapper.MappingGenerator; @@ -156,6 +157,7 @@ protected Map doFindWriters(Class clazz) { private final Types types = new Types(); private final boolean globalIsNillable; private final boolean supportsPrivateAccess; + private final int version; // CHECKSTYLE:OFF public JsonbAccessMode(final PropertyNamingStrategy propertyNamingStrategy, final String orderValue, @@ -168,6 +170,24 @@ public JsonbAccessMode(final PropertyNamingStrategy propertyNamingStrategy, fina final boolean globalIsNillable, final boolean supportsPrivateAccess) { // CHECKSTYLE:ON + this( + propertyNamingStrategy, orderValue, visibilityStrategy, caseSensitive, defaultConverters, factory, + jsonProvider, builderFactory, parserFactory, delegate, failOnMissingCreatorValues, globalIsNillable, + supportsPrivateAccess, 0); + } + + // CHECKSTYLE:OFF + public JsonbAccessMode(final PropertyNamingStrategy propertyNamingStrategy, final String orderValue, + final PropertyVisibilityStrategy visibilityStrategy, final boolean caseSensitive, + final Map> defaultConverters, final JohnzonAdapterFactory factory, + final JsonProvider jsonProvider, final Supplier builderFactory, + final Supplier parserFactory, + final AccessMode delegate, + final boolean failOnMissingCreatorValues, + final boolean globalIsNillable, + final boolean supportsPrivateAccess, + final int version) { + // CHECKSTYLE:ON this.globalIsNillable = globalIsNillable; this.naming = propertyNamingStrategy; this.order = orderValue; @@ -181,6 +201,7 @@ public JsonbAccessMode(final PropertyNamingStrategy propertyNamingStrategy, fina this.parserFactory = parserFactory; this.failOnMissingCreatorValues = failOnMissingCreatorValues; this.supportsPrivateAccess = supportsPrivateAccess; + this.version = version; } @Override @@ -953,6 +974,10 @@ private boolean isTransient(final DecoratedType t) { if (t.getAnnotation(JsonbTransient.class) != null) { return true; } + final JohnzonIgnore johnzonIgnore = t.getAnnotation(JohnzonIgnore.class); + if (johnzonIgnore != null && johnzonIgnore.minVersion() >= 0 && version < johnzonIgnore.minVersion()) { + return true; + } // TODO: spec requirement, this sounds wrong since you cant customize 2 kind of serializations on the same model if (FieldAccessMode.FieldDecoratedType.class.isInstance(t)) { final Field field = FieldAccessMode.FieldDecoratedType.class.cast(t).getField(); diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonIgnoreJsonbTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonIgnoreJsonbTest.java new file mode 100644 index 00000000..dd671c91 --- /dev/null +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JohnzonIgnoreJsonbTest.java @@ -0,0 +1,181 @@ +/* + * 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.mapper.JohnzonIgnore; +import org.apache.johnzon.mapper.JohnzonRecord; +import org.apache.johnzon.jsonb.test.JsonbRule; +import org.junit.Rule; +import org.junit.Test; + +import jakarta.json.bind.annotation.JsonbTransient; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class JohnzonIgnoreJsonbTest { + @Rule + public final JsonbRule jsonb = new JsonbRule(); + + @Rule + public final JsonbRule jsonbWithExcludedMethods = new JsonbRule() + .withProperty("johnzon.accessMode.excludedMethods", "extra") + .withProperty("johnzon.accessMode.supportAllRecordAttributes", Boolean.TRUE); + + @Rule + public final JsonbRule jsonbWithVersion0 = new JsonbRule() + .withProperty("johnzon.version", 0); + + @Rule + public final JsonbRule jsonbWithVersion2 = new JsonbRule() + .withProperty("johnzon.version", 2); + + @Test + public void johnzonIgnoreDefaultMinVersionIsTransient() { + final AlwaysIgnored obj = new AlwaysIgnored(); + obj.visible = "yes"; + obj.hidden = "no"; + final String json = jsonb.toJson(obj); + assertTrue(json.contains("\"visible\"")); + assertFalse(json.contains("\"hidden\"")); + } + + @Test + public void johnzonIgnoreWithJsonbTransientBothAreTransient() { + final BothTransient obj = new BothTransient(); + obj.field1 = "a"; + obj.field2 = "b"; + final String json = jsonb.toJson(obj); + assertFalse(json.contains("\"field1\"")); + assertFalse(json.contains("\"field2\"")); + } + + @Test + public void johnzonIgnoreVersionedRespectedViaJsonbConfig() { + final VersionedRecord obj = new VersionedRecord(); + obj.name = "test"; + obj.extra = "data"; + final String json = jsonbWithExcludedMethods.toJson(obj); + assertTrue(json.contains("\"name\"")); + assertFalse(json.contains("\"extra\"")); + } + + @Test + public void johnzonIgnoreMinVersionBelowConfiguredVersionIsNotTransient() { + final VersionedPojo obj = new VersionedPojo(); + obj.visible = "yes"; + obj.hidden = "no"; + final String json = jsonbWithVersion2.toJson(obj); + assertTrue(json.contains("\"visible\"")); + assertTrue(json.contains("\"hidden\"")); + } + + @Test + public void johnzonIgnoreMinVersionAboveConfiguredVersionIsTransient() { + final VersionedPojo obj = new VersionedPojo(); + obj.visible = "yes"; + obj.hidden = "no"; + final String json = jsonbWithVersion0.toJson(obj); + assertTrue(json.contains("\"visible\"")); + assertFalse(json.contains("\"hidden\"")); + } + + public static class AlwaysIgnored { + public String visible; + + @JohnzonIgnore + public String hidden; + + public String getVisible() { + return visible; + } + + public void setVisible(final String visible) { + this.visible = visible; + } + + public String getHidden() { + return hidden; + } + + public void setHidden(final String hidden) { + this.hidden = hidden; + } + } + + public static class BothTransient { + @JsonbTransient + public String field1; + + @JohnzonIgnore + public String field2; + + public String getField1() { + return field1; + } + + public void setField1(final String field1) { + this.field1 = field1; + } + + public String getField2() { + return field2; + } + + public void setField2(final String field2) { + this.field2 = field2; + } + } + + @JohnzonRecord + public static class VersionedRecord { + private String name; + private String extra; + + public String name() { + return name; + } + + public String extra() { + return extra; + } + } + + public static class VersionedPojo { + private String visible; + private String hidden; + + public String getVisible() { + return visible; + } + + public void setVisible(final String visible) { + this.visible = visible; + } + + @JohnzonIgnore(minVersion = 2) + public String getHidden() { + return hidden; + } + + public void setHidden(final String hidden) { + this.hidden = hidden; + } + } +} diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/test/JsonbRule.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/test/JsonbRule.java index 855d1593..295a8dc8 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/test/JsonbRule.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/test/JsonbRule.java @@ -62,6 +62,11 @@ public JsonbRule withTypeAdapter(JsonbAdapter... jsonbAdapters) { return this; } + public JsonbRule withProperty(final String key, final Object value) { + config.setProperty(key, value); + return this; + } + @Override public Statement apply(final Statement statement, final Description description) { return new Statement() { 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 9b088963..d520e2e1 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 @@ -59,6 +59,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; @@ -98,6 +99,8 @@ public class MapperBuilder { private Map, String[]> ignoredForFields = new HashMap, String[]>(); private Map, Class> interfaceImplementationMapping = new HashMap<>(); private BaseAccessMode.FieldFilteringStrategy fieldFilteringStrategy = null; + private Set accessModeExcludedMethods = null; + private boolean supportAllRecordAttributes; private boolean primitiveConverters; private boolean failOnUnknownProperties; private SerializeValueFilter serializeValueFilter; @@ -186,6 +189,27 @@ public Mapper build() { } BaseAccessMode.class.cast(accessMode).setFieldFilteringStrategy(fieldFilteringStrategy); } + if (accessModeExcludedMethods != null) { + if (MethodAccessMode.class.isInstance(accessMode)) { + MethodAccessMode.class.cast(accessMode).setExcludedMethods(accessModeExcludedMethods); + } else if (FieldAndMethodAccessMode.class.isInstance(accessMode)) { + FieldAndMethodAccessMode.class.cast(accessMode).setExcludedMethods(accessModeExcludedMethods); + } + } + if (supportAllRecordAttributes) { + if (MethodAccessMode.class.isInstance(accessMode)) { + MethodAccessMode.class.cast(accessMode).setSupportAllRecordAttributes(true); + } else if (FieldAndMethodAccessMode.class.isInstance(accessMode)) { + FieldAndMethodAccessMode.class.cast(accessMode).setSupportAllRecordAttributes(true); + } + } + if (version >= 0) { + if (MethodAccessMode.class.isInstance(accessMode)) { + MethodAccessMode.class.cast(accessMode).setVersion(version); + } else if (FieldAndMethodAccessMode.class.isInstance(accessMode)) { + FieldAndMethodAccessMode.class.cast(accessMode).setVersion(version); + } + } if (!ignoredForFields.isEmpty()) { if (BaseAccessMode.class.isInstance(accessMode)) { final BaseAccessMode baseAccessMode = BaseAccessMode.class.cast(accessMode); @@ -365,6 +389,16 @@ public MapperBuilder setAccessModeFieldFilteringStrategyName(final String mode) } } + public MapperBuilder setAccessModeExcludedMethods(final Set excludedMethods) { + this.accessModeExcludedMethods = excludedMethods; + return this; + } + + public MapperBuilder setSupportAllRecordAttributes(final boolean supportAllRecordAttributes) { + this.supportAllRecordAttributes = supportAllRecordAttributes; + return this; + } + public MapperBuilder setSupportHiddenAccess(final boolean supportHiddenAccess) { this.supportHiddenAccess = supportHiddenAccess; return this; 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 2080a797..917e031f 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 @@ -34,6 +34,7 @@ import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; +import java.util.Set; // annotated entity overrides the other one, methods are used instead of field if both are there public class FieldAndMethodAccessMode extends BaseAccessMode { @@ -66,6 +67,30 @@ public FieldAndMethodAccessMode(final boolean useConstructor, final boolean acce this(useConstructor, acceptHiddenConstructor, useGettersAsWriter, true, false); } + public Set getExcludedMethods() { + return methods.getExcludedMethods(); + } + + public void setExcludedMethods(final Set excludedMethods) { + methods.setExcludedMethods(excludedMethods); + } + + public boolean isSupportAllGetters() { + return methods.isSupportAllGetters(); + } + + public void setSupportAllRecordAttributes(final boolean supportAllRecordAttributes) { + methods.setSupportAllRecordAttributes(supportAllRecordAttributes); + } + + public int getVersion() { + return methods.getVersion(); + } + + public void setVersion(final int version) { + methods.setVersion(version); + } + @Override public Map doFindReaders(final Class clazz) { @@ -96,6 +121,15 @@ final boolean record = isRecord(clazz); break; } } + if (!skip) { + final JohnzonIgnore ignore = Meta.getAnnotation(m, JohnzonIgnore.class); + if (ignore != null) { + final int minVersion = ignore.minVersion(); + if (minVersion < 0 || (methods.getVersion() >= 0 && methods.getVersion() < minVersion)) { + skip = true; + } + } + } } else if (!ignoreVisibilityFilter && m != null) { continue; } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java index 0288f953..1e3244b5 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java @@ -30,12 +30,14 @@ import java.lang.reflect.Type; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Stream; import org.apache.johnzon.mapper.Adapter; import org.apache.johnzon.mapper.JohnzonAny; +import org.apache.johnzon.mapper.JohnzonIgnore; import org.apache.johnzon.mapper.JohnzonProperty; import org.apache.johnzon.mapper.JohnzonRecord; import org.apache.johnzon.mapper.MapperException; @@ -44,12 +46,41 @@ public class MethodAccessMode extends BaseAccessMode { private final boolean supportGetterAsWritter; + private Set excludedMethods = Set.of("toString", "hashCode"); + private boolean supportAllRecordAttributes; + private int version = -1; public MethodAccessMode(final boolean useConstructor, final boolean acceptHiddenConstructor, final boolean supportGetterAsWritter) { super(useConstructor, acceptHiddenConstructor); this.supportGetterAsWritter = supportGetterAsWritter; } + public Set getExcludedMethods() { + return excludedMethods; + } + + public void setExcludedMethods(final Set excludedMethods) { + this.excludedMethods = excludedMethods == null || excludedMethods.isEmpty() + ? Set.of("toString", "hashCode") + : new HashSet<>(excludedMethods); + } + + public boolean isSupportAllGetters() { + return supportAllRecordAttributes; + } + + public void setSupportAllRecordAttributes(final boolean supportAllRecordAttributes) { + this.supportAllRecordAttributes = supportAllRecordAttributes; + } + + public int getVersion() { + return version; + } + + public void setVersion(final int version) { + this.version = version; + } + @Override public Map doFindReaders(final Class clazz) { final Map readers = new HashMap<>(); @@ -60,9 +91,10 @@ public Map doFindReaders(final Class clazz) { readers.putAll(Stream.of(clazz.getMethods()) .filter(it -> it.getDeclaringClass() != Object.class && it.getParameterCount() == 0) .filter(it -> !Modifier.isStatic(it.getModifiers())) - .filter(it -> components == null || components.contains(it.getName())) - .filter(it -> !"toString".equals(it.getName()) && !"hashCode".equals(it.getName())) + .filter(it -> supportAllRecordAttributes || components == null || components.contains(it.getName())) + .filter(it -> !excludedMethods.contains(it.getName())) .filter(it -> !isIgnored(it.getName()) && Meta.getAnnotation(it, JohnzonAny.class) == null) + .filter(it -> !isAlwaysIgnored(it)) .collect(toMap(m -> extractKey(m.getName(), m, null), it -> new MethodReader(it, it.getGenericReturnType())))); } else { final PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(clazz); @@ -70,7 +102,8 @@ public Map doFindReaders(final Class clazz) { final Method readMethod = descriptor.getReadMethod(); final String name = descriptor.getName(); if (readMethod != null && readMethod.getDeclaringClass() != Object.class) { - if (isIgnored(name) || Meta.getAnnotation(readMethod, JohnzonAny.class) != null) { + if (isIgnored(name) || Meta.getAnnotation(readMethod, JohnzonAny.class) != null + || isAlwaysIgnored(readMethod)) { continue; } readers.put(extractKey(name, readMethod, null), new MethodReader(readMethod, readMethod.getGenericReturnType())); @@ -79,7 +112,9 @@ public Map doFindReaders(final Class clazz) { try { final Method method = clazz.getMethod( "is" + Character.toUpperCase(name.charAt(0)) + (name.length() > 1 ? name.substring(1) : "")); - readers.put(extractKey(name, method, null), new MethodReader(method, method.getGenericReturnType())); + if (!isAlwaysIgnored(method)) { + readers.put(extractKey(name, method, null), new MethodReader(method, method.getGenericReturnType())); + } } catch (final NoSuchMethodException e) { // no-op } @@ -123,6 +158,18 @@ protected boolean isIgnored(final String name) { return name.equals("metaClass") || name.contains("$"); } + private boolean isAlwaysIgnored(final Method method) { + final JohnzonIgnore ignore = Meta.getAnnotation(method, JohnzonIgnore.class); + if (ignore == null) { + return false; + } + final int minVersion = ignore.minVersion(); + if (minVersion < 0) { + return true; + } + return version >= 0 && version < minVersion; + } + private PropertyDescriptor[] getPropertyDescriptors(final Class clazz) { final PropertyDescriptor[] propertyDescriptors; try { diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/access/MethodAccessModeTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/access/MethodAccessModeTest.java new file mode 100644 index 00000000..0a4a0164 --- /dev/null +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/access/MethodAccessModeTest.java @@ -0,0 +1,437 @@ +/* + * 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.apache.johnzon.mapper.JohnzonIgnore; +import org.apache.johnzon.mapper.JohnzonRecord; +import org.apache.johnzon.mapper.Mapper; +import org.apache.johnzon.mapper.MapperBuilder; +import org.junit.Test; + +import java.io.StringWriter; +import java.util.Map; +import java.util.Set; + +import org.apache.johnzon.mapper.access.AccessMode.Reader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MethodAccessModeTest { + @Test + public void defaultExcludedMethods() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + assertEquals(Set.of("toString", "hashCode"), mode.getExcludedMethods()); + } + + @Test + public void customExcludedMethods() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setExcludedMethods(Set.of("foo", "bar")); + assertEquals(Set.of("foo", "bar"), mode.getExcludedMethods()); + } + + @Test + public void excludedMethodsNullResetsToDefault() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setExcludedMethods(null); + assertEquals(Set.of("toString", "hashCode"), mode.getExcludedMethods()); + } + + @Test + public void excludedMethodsEmptyResetsToDefault() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setExcludedMethods(Set.of()); + assertEquals(Set.of("toString", "hashCode"), mode.getExcludedMethods()); + } + + @Test + public void excludedMethodsAreNotInReadersForRecords() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + final Map readers = mode.findReaders(JohnzonRecordSimple.class); + assertFalse(readers.containsKey("toString")); + assertFalse(readers.containsKey("hashCode")); + assertTrue(readers.containsKey("name")); + } + + @Test + public void customExcludedMethodIsNotInReadersForRecords() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setExcludedMethods(Set.of("name")); + final Map readers = mode.findReaders(JohnzonRecordSimple.class); + assertFalse(readers.containsKey("name")); + assertTrue(readers.containsKey("extra")); + } + + @Test + public void excludedMethodsWithMapperBuilderMethodMode() { + final Mapper mapper = new MapperBuilder() + .setAccessModeName("method") + .setAccessModeExcludedMethods(Set.of("extra")) + .build(); + final StringWriter writer = new StringWriter(); + final JohnzonRecordSimple obj = new JohnzonRecordSimple(); + obj.name = "test"; + obj.extra = "data"; + mapper.writeObject(obj, writer); + assertTrue(writer.toString().contains("\"name\"")); + assertFalse(writer.toString().contains("\"extra\"")); + } + + @Test + public void excludedMethodsHaveNoEffectOnNonRecordPropertyDescriptors() { + final FieldAndMethodAccessMode mode = new FieldAndMethodAccessMode(true, true, false, true, false); + mode.setExcludedMethods(Set.of("foo")); + final Map readers = mode.findReaders(Pojo.class); + // "foo" is a standard bean property, excludedMethods only applies in the record branch + assertTrue(readers.containsKey("foo")); + } + + @Test + public void johnzonIgnoreOnNonRecordPojoMethod() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + final Map readers = mode.findReaders(PojoWithIgnore.class); + assertTrue(readers.containsKey("foo")); + assertFalse(readers.containsKey("bar")); + } + + @Test + public void johnzonIgnoreVersionedOnNonRecordPojoMethod() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setVersion(1); + final Map readers = mode.findReaders(PojoWithVersionedIgnore.class); + assertTrue(readers.containsKey("foo")); + assertFalse(readers.containsKey("bar")); + + final MethodAccessMode mode2 = new MethodAccessMode(true, true, true); + mode2.setVersion(3); + final Map readers2 = mode2.findReaders(PojoWithVersionedIgnore.class); + assertTrue(readers2.containsKey("foo")); + assertTrue(readers2.containsKey("bar")); + } + + @Test + public void johnzonIgnoreOnBooleanIsMethod() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + final Map readers = mode.findReaders(PojoWithIgnoredIsMethod.class); + assertTrue(readers.containsKey("active")); + assertFalse(readers.containsKey("deleted")); + } + + @Test + public void johnzonIgnoreVersionedOnBooleanIsMethod() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setVersion(1); + final Map readers = mode.findReaders(PojoWithVersionedIgnoredIsMethod.class); + assertTrue(readers.containsKey("active")); + assertFalse(readers.containsKey("deleted")); + + final MethodAccessMode mode2 = new MethodAccessMode(true, true, true); + mode2.setVersion(3); + final Map readers2 = mode2.findReaders(PojoWithVersionedIgnoredIsMethod.class); + assertTrue(readers2.containsKey("active")); + assertTrue(readers2.containsKey("deleted")); + } + + @Test + public void johnzonIgnoreDefaultMinVersionAlwaysIgnored() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + final Map readers = mode.findReaders(WithJohnzonIgnoreAlways.class); + assertFalse(readers.containsKey("hidden")); + assertTrue(readers.containsKey("visible")); + } + + @Test + public void johnzonIgnoreMinVersionBelowMapperVersion() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setVersion(2); + final Map readers = mode.findReaders(WithJohnzonIgnoreVersioned.class); + assertTrue(readers.containsKey("hidden")); + assertTrue(readers.containsKey("visible")); + } + + @Test + public void johnzonIgnoreMinVersionAboveMapperVersion() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setVersion(1); + final Map readers = mode.findReaders(WithJohnzonIgnoreVersioned.class); + assertFalse(readers.containsKey("hidden")); + assertTrue(readers.containsKey("visible")); + } + + @Test + public void johnzonIgnoreMinVersionWithMethodAccessModeDirectly() { + final Mapper mapper = new MapperBuilder() + .setAccessModeName("method") + .setVersion(0) + .build(); + final StringWriter writer = new StringWriter(); + final WithJohnzonIgnoreVersioned obj = new WithJohnzonIgnoreVersioned(); + obj.hidden = "secret"; + obj.visible = "public"; + mapper.writeObject(obj, writer); + assertTrue(writer.toString().contains("\"visible\"")); + assertFalse(writer.toString().contains("\"hidden\"")); + } + + @Test + public void johnzonIgnoreMinVersionMetWithMethodAccessMode() { + final Mapper mapper = new MapperBuilder() + .setAccessModeName("method") + .setVersion(2) + .build(); + final StringWriter writer = new StringWriter(); + final WithJohnzonIgnoreVersioned obj = new WithJohnzonIgnoreVersioned(); + obj.hidden = "secret"; + obj.visible = "public"; + mapper.writeObject(obj, writer); + assertTrue(writer.toString().contains("\"visible\"")); + assertTrue(writer.toString().contains("\"hidden\"")); + } + + @Test + public void johnzonIgnoreVersionedEndToEndDefaultAccessMode() { + final Mapper mapper = new MapperBuilder().setVersion(0).build(); + final StringWriter writer = new StringWriter(); + final WithJohnzonIgnoreVersioned obj = new WithJohnzonIgnoreVersioned(); + obj.hidden = "secret"; + obj.visible = "public"; + mapper.writeObject(obj, writer); + assertTrue(writer.toString().contains("\"visible\"")); + assertFalse(writer.toString().contains("\"hidden\"")); + } + + @Test + public void johnzonIgnoreVersionedMetEndToEndDefaultAccessMode() { + final Mapper mapper = new MapperBuilder().setVersion(2).build(); + final StringWriter writer = new StringWriter(); + final WithJohnzonIgnoreVersioned obj = new WithJohnzonIgnoreVersioned(); + obj.hidden = "secret"; + obj.visible = "public"; + mapper.writeObject(obj, writer); + assertTrue(writer.toString().contains("\"visible\"")); + assertTrue(writer.toString().contains("\"hidden\"")); + } + + @Test + public void fieldAndMethodAccessModeAlwaysIgnoredMethodSuppressesFieldReader() { + final FieldAndMethodAccessMode mode = new FieldAndMethodAccessMode(true, true, false, true, false); + final Map readers = mode.findReaders(PojoWithIgnore.class); + assertTrue(readers.containsKey("foo")); + assertFalse(readers.containsKey("bar")); + } + + @Test + public void fieldAndMethodAccessModeVersionedIgnoreSuppressesFieldReader() { + final FieldAndMethodAccessMode mode = new FieldAndMethodAccessMode(true, true, false, true, false); + mode.setVersion(1); + final Map readers = mode.findReaders(PojoWithVersionedIgnore.class); + assertTrue(readers.containsKey("foo")); + assertFalse(readers.containsKey("bar")); + + final FieldAndMethodAccessMode mode2 = new FieldAndMethodAccessMode(true, true, false, true, false); + mode2.setVersion(3); + final Map readers2 = mode2.findReaders(PojoWithVersionedIgnore.class); + assertTrue(readers2.containsKey("foo")); + assertTrue(readers2.containsKey("bar")); + } + + @Test + public void fieldAndMethodAccessModeVersionPassThrough() { + final FieldAndMethodAccessMode mode = new FieldAndMethodAccessMode(true, true, false, true, false); + mode.setVersion(5); + assertEquals(5, mode.getVersion()); + } + + @Test + public void fieldAndMethodAccessModeExcludedMethodsPassThrough() { + final FieldAndMethodAccessMode mode = new FieldAndMethodAccessMode(true, true, false, true, false); + mode.setExcludedMethods(Set.of("custom")); + assertEquals(Set.of("custom"), mode.getExcludedMethods()); + } + + @Test + public void fieldAndMethodAccessModeSupportAllGettersPassThrough() { + final FieldAndMethodAccessMode mode = new FieldAndMethodAccessMode(true, true, false, true, false); + mode.setSupportAllRecordAttributes(true); + assertTrue(mode.isSupportAllGetters()); + } + + @Test + public void supportAllRecordAttributesNoEffectOnJohnzonRecord() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setSupportAllRecordAttributes(false); + final Map readers = mode.findReaders(JohnzonRecordSimple.class); + // @JohnzonRecord returns null componentNames, so supportAllRecordAttributes has no effect + assertTrue(readers.containsKey("name")); + assertTrue(readers.containsKey("extra")); + } + + @Test + public void supportAllRecordAttributesTrueAlsoIncludesAllForJohnzonRecord() { + final MethodAccessMode mode = new MethodAccessMode(true, true, true); + mode.setSupportAllRecordAttributes(true); + final Map readers = mode.findReaders(JohnzonRecordSimple.class); + assertTrue(readers.containsKey("name")); + assertTrue(readers.containsKey("extra")); + } + + @JohnzonRecord + public static class JohnzonRecordSimple { + private String name; + private String extra; + + public String name() { + return name; + } + + public String extra() { + return extra; + } + } + + public static class Pojo { + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(final String foo) { + this.foo = foo; + } + } + + public static class PojoWithIgnore { + private String foo; + private String bar; + + public String getFoo() { + return foo; + } + + public void setFoo(final String foo) { + this.foo = foo; + } + + @JohnzonIgnore + public String getBar() { + return bar; + } + + public void setBar(final String bar) { + this.bar = bar; + } + } + + public static class PojoWithVersionedIgnore { + private String foo; + private String bar; + + public String getFoo() { + return foo; + } + + public void setFoo(final String foo) { + this.foo = foo; + } + + @JohnzonIgnore(minVersion = 2) + public String getBar() { + return bar; + } + + public void setBar(final String bar) { + this.bar = bar; + } + } + + public static class PojoWithIgnoredIsMethod { + private boolean active; + private boolean deleted; + + public boolean isActive() { + return active; + } + + public void setActive(final boolean active) { + this.active = active; + } + + @JohnzonIgnore + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(final boolean deleted) { + this.deleted = deleted; + } + } + + public static class PojoWithVersionedIgnoredIsMethod { + private boolean active; + private boolean deleted; + + public boolean isActive() { + return active; + } + + public void setActive(final boolean active) { + this.active = active; + } + + @JohnzonIgnore(minVersion = 2) + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(final boolean deleted) { + this.deleted = deleted; + } + } + + public static class WithJohnzonIgnoreAlways { + String visible; + String hidden; + + public String getVisible() { + return visible; + } + + @JohnzonIgnore + public String getHidden() { + return hidden; + } + } + + public static class WithJohnzonIgnoreVersioned { + String visible; + String hidden; + + public String getVisible() { + return visible; + } + + @JohnzonIgnore(minVersion = 2) + public String getHidden() { + return hidden; + } + } +}