From dcf82b795ced2e570ba70bfe087d138d9033ea20 Mon Sep 17 00:00:00 2001 From: Baptiste Pernet Date: Wed, 22 Jul 2020 18:55:48 -0700 Subject: [PATCH] FasterXML/jackson-databind#1296 @JsonIncludeProperties (#174) Add `@JsonIncludeProperties` --- .../annotation/JsonIncludeProperties.java | 167 ++++++++++++++++++ .../annotation/JsonIncludePropertiesTest.java | 68 +++++++ 2 files changed, 235 insertions(+) create mode 100644 src/main/java/com/fasterxml/jackson/annotation/JsonIncludeProperties.java create mode 100644 src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonIncludeProperties.java b/src/main/java/com/fasterxml/jackson/annotation/JsonIncludeProperties.java new file mode 100644 index 00000000..1ffaec54 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonIncludeProperties.java @@ -0,0 +1,167 @@ +package com.fasterxml.jackson.annotation; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import javax.swing.text.html.HTMLDocument; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Annotation that can be used to either only include serialization of + * properties (during serialization), or only include processing of + * JSON properties read (during deserialization). + *

+ * Example: + *

+ * // to only include specified fields from being serialized or deserialized
+ * // (i.e. only include in JSON output; or being set even if they were included)
+ * @JsonIncludeProperties({ "internalId", "secretKey" })
+ * 
+ *

+ * Annotation can be applied both to classes and + * to properties. If used for both, actual set will be union of all + * includes: that is, you can only add properties to include, not remove + * or override. So you can not remove properties to include using + * per-property annotation. + * + * @since 2.12 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, + ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonIncludeProperties +{ + /** + * Names of properties to include. + */ + public String[] value() default {}; + + /* + /********************************************************** + /* Value class used to enclose information, allow for + /* merging of layered configuration settings. + /********************************************************** + */ + + /** + * Helper class used to contain information from a single {@link JsonIncludeProperties} + * annotation, as well as to provide possible overrides from non-annotation sources. + * + * @since 2.12 + */ + public static class Value implements JacksonAnnotationValue, java.io.Serializable + { + private static final long serialVersionUID = 1L; + + /** + * Default instance has no explicitly included fields + */ + protected final static JsonIncludeProperties.Value ALL = new JsonIncludeProperties.Value(null); + + /** + * Name of the properties to include. + * Null means that all properties are included, empty means none. + */ + protected final Set _included; + + protected Value(Set included) + { + _included = included; + } + + public static JsonIncludeProperties.Value from(JsonIncludeProperties src) + { + if (src == null) { + return ALL; + } + + return new Value(_asSet(src.value())); + } + + public static JsonIncludeProperties.Value all() + { + return ALL; + } + + @Override + public Class valueFor() + { + return JsonIncludeProperties.class; + } + + public Set getIncluded() + { + return _included; + } + + /** + * Mutant factory method to override the current value with an another, merging the included fields. + */ + public JsonIncludeProperties.Value withOverrides(JsonIncludeProperties.Value overrides) { + if (overrides == null || overrides.getIncluded() == null) { + return this; + } + + if (_included == null) { + return overrides; + } + + HashSet included = new HashSet(_included); + Iterator iterator = included.iterator(); + while (iterator.hasNext()) { + if (!overrides.getIncluded().contains(iterator.next())) { + iterator.remove(); + } + } + + return new JsonIncludeProperties.Value(new HashSet(included)); + } + + @Override + public String toString() { + return String.format("JsonIncludeProperties.Value(included=%s)", + _included); + } + + @Override + public int hashCode() { + return (_included == null ? 0 : _included.size()) + ; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null) return false; + return (o.getClass() == getClass()) + && _equals(this, (Value) o); + } + + private static boolean _equals(Value a, Value b) + { + return a._included == null ? b._included == null : + // keep this last just because it can be expensive + a._included.equals(b._included) + ; + } + + private static Set _asSet(String[] v) + { + if (v == null || v.length == 0) { + return Collections.emptySet(); + } + Set s = new HashSet(v.length); + for (String str : v) { + s.add(str); + } + return s; + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java new file mode 100644 index 00000000..55ae7387 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java @@ -0,0 +1,68 @@ +package com.fasterxml.jackson.annotation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Tests to verify that it is possibly to merge {@link JsonIncludeProperties.Value} + * instances for overrides + */ +public class JsonIncludePropertiesTest extends TestBase +{ + @JsonIncludeProperties(value = {"foo", "bar"}) + private final static class Bogus + { + } + + private final JsonIncludeProperties.Value ALL = JsonIncludeProperties.Value.all(); + + public void testAll() + { + assertSame(ALL, JsonIncludeProperties.Value.from(null)); + assertNull(ALL.getIncluded()); + assertEquals(ALL, ALL); + assertEquals("JsonIncludeProperties.Value(included=null)", ALL.toString()); + assertEquals(0, ALL.hashCode()); + } + + public void testFromAnnotation() + { + JsonIncludeProperties.Value v = JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class)); + assertNotNull(v); + Set included = v.getIncluded(); + assertEquals(2, v.getIncluded().size()); + assertEquals(_set("foo", "bar"), included); + assertEquals("JsonIncludeProperties.Value(included=[bar, foo])", v.toString()); + assertEquals(v, JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class))); + } + + public void testWithOverridesAll() { + JsonIncludeProperties.Value v = JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class)); + v = v.withOverrides(ALL); + Set included = v.getIncluded(); + assertEquals(2, included.size()); + assertEquals(_set("foo", "bar"), included); + } + + public void testWithOverridesEmpty() { + JsonIncludeProperties.Value v = JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class)); + v = v.withOverrides(new JsonIncludeProperties.Value(Collections.emptySet())); + Set included = v.getIncluded(); + assertEquals(0, included.size()); + } + + public void testWithOverridesMerge() { + JsonIncludeProperties.Value v = JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class)); + v = v.withOverrides(new JsonIncludeProperties.Value(_set("foo"))); + Set included = v.getIncluded(); + assertEquals(1, included.size()); + assertEquals(_set("foo"), included); + } + + private Set _set(String... args) + { + return new LinkedHashSet(Arrays.asList(args)); + } +}