Skip to content

Commit

Permalink
Implement #113: add @JsonMerge (change to earlier addition to `@Jso…
Browse files Browse the repository at this point in the history
…nSetter`)
  • Loading branch information
cowtowncoder committed Feb 1, 2017
1 parent e057094 commit a7776f1
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 114 deletions.
3 changes: 2 additions & 1 deletion release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ NOTE: Annotations module will never contain changes in patch versions,
2.9.0 (not yet released)

#103: Add `JsonInclude.Include.CUSTOM`, properties for specifying filter(s) to use
#104: Add new properties in `@JsonSetter`: `merge`, `null`/`contentNulls`
#104: Add new properties in `@JsonSetter`: `nulls`/`contentNulls`
#105: Add `@JsonFormat.lenient` to allow configuring lenience of date/time deserializers
#108: Allow `@JsonValue` on fields
#109: Add `enabled` for `@JsonAnyGetter`, `@JsonAnySetter`, to allow disabling via mix-ins
#113: Add `@JsonMerge` to support (deep) merging of properties
- Allow use of `@JsonView` on classes, to specify Default View to use on non-annotated
properties.

Expand Down
51 changes: 51 additions & 0 deletions src/main/java/com/fasterxml/jackson/annotation/JsonMerge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.fasterxml.jackson.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to specify whether annotated property value should use "merging" approach,
* in which current value is first accessed (with a getter or field) and then modified
* with incoming data, or not: if not, assignment happens without considering current state.
*<p>
* Merging is only option if there is a way to introspect current state:
* if there is accessor (getter, field) to use.
* Merging can not be enabled if no accessor exists
* or if assignment occurs using a Creator setter (constructor
* or factory method), since there is no instance with state to introspect.
* Merging also only has actual effect for structured types where there is an
* obvious way to update a state (for example, POJOs have default values for properties,
* and {@link java.util.Collection}s and {@link java.util.Map}s may have existing
* elements; whereas scalar types do not such state: an <code>int</code> has a value,
* but no obvious and non-ambiguous way to merge state.
*<p>
* Merging is applied by using a deserialization method that accepts existing state
* as an argument: it is then up to <code>JsonDeserializer</code> implementation
* to use that base state in a way that makes sense without further configuration.
* For structured types this is usually obvious; and for scalar types not -- if
* no obvious method exists, merging is not allowed; deserializer may choose to
* either quietly ignore it, or throw an exception.
*<p>
* Note that use of merging usually adds some processing overhead since it adds
* an extra step of accessing the current state before assignment.
*<p>
* Note also that "root values" (values directly deserialized and not reached
* via POJO properties) can not use this annotation; instead, <code>ObjectMapper</code>
* and <code>Object</code> have "updating reader" operations.
*<p>
* Default value is {@link OptBoolean#TRUE}, that is, merging <b>is enabled</b>.
*
* @since 2.9
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface JsonMerge
{
/**
* Whether merging should or should not be enabled for the annotated property.
*/
OptBoolean value() default OptBoolean.TRUE;
}
106 changes: 21 additions & 85 deletions src/main/java/com/fasterxml/jackson/annotation/JsonSetter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
* {@link JsonProperty} annotation;
* or (as of 2.9 and later), specify additional aspects of the
* assigning property a value during serialization.
*<p>
*
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
// ^^^ allowed on Fields, (constructor) parameters since 2.9
Expand Down Expand Up @@ -47,35 +45,6 @@
* is usually {@link Nulls#SET}, meaning that the `null` is included as usual.
*/
Nulls contentNulls() default Nulls.DEFAULT;

/**
* Specifies whether property value should use "merging" approach, in which
* current value is first accessed (with a getter), or not; if not,
* assignment happens without considering current state.
* Merging is not an option if there is no way to introspect
* current state: for example, if there is no
* accessor to use, or if assignment occurs using a Creator setter (constructor
* or factory method), since there is no instance with state to introspect.
* Merging also only has actual effect for structured types where there is an
* obvious way to update a state (for example, POJOs have default values for properties,
* and {@link java.util.Collection}s and {@link java.util.Map}s may have existing
* elements; whereas scalar types do not such state: an <code>int</code> has a value,
* but no obvious and non-ambiguous way to merge state.
*<p>
* Merging is applied by using a deserialization method that accepts existing state
* as an argument: it is then up to <code>JsonDeserializer</code> implementation
* to use that base state in a way that makes sense without further configuration.
* For structured types this is usually obvious; and for scalar types not -- if
* no obvious method exists, merging is not allowed; deserializer may choose to
* either quietly ignore it, or throw an exception.
*<p>
* Note also that use of merging usually adds some processing overhead since it adds
* an extra step of accessing the current state before assignment.
*<p>
* Default value is to "use defaults"; in absence of any explicit configuration
* this would mean {@link OptBoolean#FALSE}, that is, merging is <b>not</b> enabled.
*/
OptBoolean merge() default OptBoolean.DEFAULT;

/*
/**********************************************************
Expand Down Expand Up @@ -144,26 +113,23 @@ public static class Value
{
private static final long serialVersionUID = 1L;

private final Boolean _merge;

private final Nulls _nulls;

private final Nulls _contentNulls;

/**
* Default instance used in place of "default settings".
*/
protected final static Value EMPTY = new Value(null, Nulls.DEFAULT, Nulls.DEFAULT);
protected final static Value EMPTY = new Value(Nulls.DEFAULT, Nulls.DEFAULT);

protected Value(Boolean merge, Nulls nulls, Nulls contentNulls) {
_merge = merge;
protected Value(Nulls nulls, Nulls contentNulls) {
_nulls = nulls;
_contentNulls = contentNulls;
}

// for JDK serialization
protected Object readResolve() {
if (_empty(_merge, _nulls, _contentNulls)) {
if (_empty(_nulls, _contentNulls)) {
return EMPTY;
}
return this;
Expand All @@ -173,8 +139,7 @@ public static Value from(JsonSetter src) {
if (src == null) {
return EMPTY;
}
return construct(src.merge().asBoolean(),
src.nulls(), src.contentNulls());
return construct(src.nulls(), src.contentNulls());
}

/**
Expand All @@ -184,24 +149,22 @@ public static Value from(JsonSetter src) {
* methods, as this factory method may need to be changed if new properties
* are added in {@link JsonIgnoreProperties} annotation.
*/
public static Value construct(Boolean merge, Nulls nulls, Nulls contentNulls) {
public static Value construct(Nulls nulls, Nulls contentNulls) {
if (nulls == null) {
nulls = Nulls.DEFAULT;
}
if (contentNulls == null) {
contentNulls = Nulls.DEFAULT;
}
if (_empty(merge, nulls, contentNulls)) {
if (_empty(nulls, contentNulls)) {
return EMPTY;
}
return new Value(merge, nulls, contentNulls);
return new Value(nulls, contentNulls);
}

/**
* Accessor for default instances which has "empty" settings; that is:
*<ul>
* <li>No definition for `merge` (that is, `null`, from {@link OptBoolean#DEFAULT})
* </li>
* <li>Null handling using global defaults, {@link Nulls#DEFAULT}.
* </li>
* </ul>
Expand All @@ -226,29 +189,17 @@ public static Value merge(Value base, Value overrides)
}

public static Value forValueNulls(Nulls nulls) {
return construct(null, nulls, Nulls.DEFAULT);
return construct(nulls, Nulls.DEFAULT);
}

public static Value forValueNulls(Nulls nulls, Nulls contentNulls) {
return construct(null, nulls, contentNulls);
return construct(nulls, contentNulls);
}

public static Value forContentNulls(Nulls nulls) {
return construct(null, Nulls.DEFAULT, nulls);
}

public static Value forMerging(Boolean merge) {
return construct(merge, Nulls.DEFAULT, Nulls.DEFAULT);
return construct(Nulls.DEFAULT, nulls);
}

public static Value forMerging() {
return construct(Boolean.TRUE, Nulls.DEFAULT, Nulls.DEFAULT);
}

public static Value forNonMerging() {
return construct(Boolean.FALSE, Nulls.DEFAULT, Nulls.DEFAULT);
}

/**
* Mutant factory method that merges values of this value with given override
* values, so that any explicitly defined inclusion in overrides has precedence over
Expand All @@ -259,24 +210,20 @@ public Value withOverrides(Value overrides) {
if ((overrides == null) || (overrides == EMPTY)) {
return this;
}
Boolean merge = overrides._merge;
Nulls nulls = overrides._nulls;
Nulls contentNulls = overrides._contentNulls;

if (merge == null) {
merge = _merge;
}
if (nulls == Nulls.DEFAULT) {
nulls = _nulls;
}
if (contentNulls == Nulls.DEFAULT) {
contentNulls = _contentNulls;
}

if ((merge == _merge) && (nulls == _nulls) && (contentNulls == _contentNulls)) {
if ((nulls == _nulls) && (contentNulls == _contentNulls)) {
return this;
}
return construct(merge, nulls, contentNulls);
return construct(nulls, contentNulls);
}

public Value withValueNulls(Nulls nulls) {
Expand All @@ -286,7 +233,7 @@ public Value withValueNulls(Nulls nulls) {
if (nulls == _nulls) {
return this;
}
return construct(_merge, nulls, _contentNulls);
return construct(nulls, _contentNulls);
}

public Value withValueNulls(Nulls valueNulls, Nulls contentNulls) {
Expand All @@ -299,7 +246,7 @@ public Value withValueNulls(Nulls valueNulls, Nulls contentNulls) {
if ((valueNulls == _nulls) && (contentNulls == _contentNulls)) {
return this;
}
return construct(_merge, valueNulls, contentNulls);
return construct(valueNulls, contentNulls);
}

public Value withContentNulls(Nulls nulls) {
Expand All @@ -309,18 +256,11 @@ public Value withContentNulls(Nulls nulls) {
if (nulls == _contentNulls) {
return this;
}
return construct(_merge, _nulls, nulls);
}

public Value withMerge(Boolean merge) {
return (merge == _merge) ? this : construct(merge, _nulls, _contentNulls);
return construct(_nulls, nulls);
}

public Nulls getValueNulls() { return _nulls; }
public Nulls getContentNulls() { return _contentNulls; }
public Boolean getMerge() { return _merge; }

public boolean shouldMerge() { return (_merge != null) && _merge.booleanValue(); }

/**
* Returns same as {@link #getValueNulls()} unless value would be
Expand All @@ -345,15 +285,13 @@ public Class<JsonSetter> valueFor() {

@Override
public String toString() {
return String.format("JsonSetter.Value(merge=%s,valueNulls=%s,contentNulls=%s)",
_merge, _nulls, _contentNulls);
return String.format("JsonSetter.Value(valueNulls=%s,contentNulls=%s)",
_nulls, _contentNulls);
}

@Override
public int hashCode() {
return (_merge == null) ? 1 : (_merge.booleanValue() ? 30 : -30)
+ _nulls.ordinal()
+ (_contentNulls.ordinal() << 2);
return _nulls.ordinal() + (_contentNulls.ordinal() << 2);
}

@Override
Expand All @@ -362,16 +300,14 @@ public boolean equals(Object o) {
if (o == null) return false;
if (o.getClass() == getClass()) {
Value other = (Value) o;
return (other._merge == _merge)
&& (other._nulls == _nulls)
return (other._nulls == _nulls)
&& (other._contentNulls == _contentNulls);
}
return false;
}

private static boolean _empty(Boolean merge, Nulls nulls, Nulls contentNulls) {
return (merge == null)
&& (nulls == Nulls.DEFAULT)
private static boolean _empty(Nulls nulls, Nulls contentNulls) {
return (nulls == Nulls.DEFAULT)
&& (contentNulls == Nulls.DEFAULT);
}
}
Expand Down
31 changes: 3 additions & 28 deletions src/test/java/com/fasterxml/jackson/annotation/JsonSetterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
public class JsonSetterTest extends TestBase
{
private final static class Bogus {
@JsonSetter(nulls=Nulls.FAIL, contentNulls=Nulls.SKIP, merge=OptBoolean.TRUE)
@JsonSetter(nulls=Nulls.FAIL, contentNulls=Nulls.SKIP)
public int field;
}

Expand All @@ -15,8 +15,6 @@ public void testEmpty()
{
assertEquals(JsonSetter.Nulls.DEFAULT, EMPTY.getValueNulls());
assertEquals(JsonSetter.Nulls.DEFAULT, EMPTY.getContentNulls());
assertNull(EMPTY.getMerge());
assertFalse(EMPTY.shouldMerge());

assertEquals(JsonSetter.class, EMPTY.valueFor());

Expand All @@ -25,7 +23,7 @@ public void testEmpty()
}

public void testStdMethods() {
assertEquals("JsonSetter.Value(merge=null,valueNulls=DEFAULT,contentNulls=DEFAULT)",
assertEquals("JsonSetter.Value(valueNulls=DEFAULT,contentNulls=DEFAULT)",
EMPTY.toString());
int x = EMPTY.hashCode();
if (x == 0) { // no fixed value, but should not evalute to 0
Expand All @@ -44,12 +42,11 @@ public void testFromAnnotation() throws Exception
JsonSetter.Value v = JsonSetter.Value.from(ann);
assertEquals(JsonSetter.Nulls.FAIL, v.getValueNulls());
assertEquals(JsonSetter.Nulls.SKIP, v.getContentNulls());
assertEquals(Boolean.TRUE, v.getMerge());
}

public void testConstruct() throws Exception
{
JsonSetter.Value v = JsonSetter.Value.construct(null, null, null);
JsonSetter.Value v = JsonSetter.Value.construct(null, null);
assertSame(EMPTY, v);
}

Expand All @@ -58,25 +55,12 @@ public void testFactories() throws Exception
JsonSetter.Value v = JsonSetter.Value.forContentNulls(Nulls.SET);
assertEquals(Nulls.DEFAULT, v.getValueNulls());
assertEquals(Nulls.SET, v.getContentNulls());
assertNull(v.getMerge());
assertEquals(Nulls.SET, v.nonDefaultContentNulls());

JsonSetter.Value skip = JsonSetter.Value.forValueNulls(Nulls.SKIP);
assertEquals(Nulls.SKIP, skip.getValueNulls());
assertEquals(Nulls.DEFAULT, skip.getContentNulls());
assertEquals(Nulls.SKIP, skip.nonDefaultValueNulls());
assertNull(skip.getMerge());

JsonSetter.Value merging = JsonSetter.Value.forMerging();
assertEquals(Nulls.DEFAULT, merging.getValueNulls());
assertEquals(Nulls.DEFAULT, merging.getContentNulls());
assertEquals(Boolean.TRUE, merging.getMerge());

assertFalse(skip.equals(merging));
assertFalse(merging.equals(skip));

assertFalse(merging.equals(EMPTY));
assertFalse(EMPTY.equals(merging));
}

public void testSimpleMerge()
Expand All @@ -85,15 +69,6 @@ public void testSimpleMerge()
assertEquals(Nulls.SKIP, v.getContentNulls());
v = v.withValueNulls(Nulls.FAIL);
assertEquals(Nulls.FAIL, v.getValueNulls());
v = v.withMerge(Boolean.FALSE);
assertEquals(Boolean.FALSE, v.getMerge());

JsonSetter.Value override = JsonSetter.Value.forMerging(Boolean.TRUE);
JsonSetter.Value merged = JsonSetter.Value.merge(v, override);
assertEquals(Boolean.TRUE, merged.getMerge());
assertEquals(Nulls.SKIP, merged.getContentNulls());
assertEquals(Nulls.FAIL, merged.getValueNulls());
assertTrue(merged.shouldMerge());
}

public void testWithMethods()
Expand Down

0 comments on commit a7776f1

Please sign in to comment.