Skip to content

Commit

Permalink
Improve versioning support documentation and validate version (#2214)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcono1234 committed Oct 2, 2022
1 parent 2860908 commit 796193d
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 64 deletions.
23 changes: 18 additions & 5 deletions gson/src/main/java/com/google/gson/GsonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE;

import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
import com.google.gson.internal.$Gson$Preconditions;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.bind.DefaultDateTypeAdapter;
Expand Down Expand Up @@ -143,14 +145,25 @@ public GsonBuilder() {
}

/**
* Configures Gson to enable versioning support.
* Configures Gson to enable versioning support. Versioning support works based on the
* annotation types {@link Since} and {@link Until}. It allows including or excluding fields
* and classes based on the specified version. See the documentation of these annotation
* types for more information.
*
* @param ignoreVersionsAfter any field or type marked with a version higher than this value
* are ignored during serialization or deserialization.
* <p>By default versioning support is disabled and usage of {@code @Since} and {@code @Until}
* has no effect.
*
* @param version the version number to use.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @throws IllegalArgumentException if the version number is NaN or negative
* @see Since
* @see Until
*/
public GsonBuilder setVersion(double ignoreVersionsAfter) {
excluder = excluder.withVersion(ignoreVersionsAfter);
public GsonBuilder setVersion(double version) {
if (Double.isNaN(version) || version < 0.0) {
throw new IllegalArgumentException("Invalid version: " + version);
}
excluder = excluder.withVersion(version);
return this;
}

Expand Down
12 changes: 7 additions & 5 deletions gson/src/main/java/com/google/gson/annotations/Since.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.gson.annotations;

import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -24,12 +25,11 @@

/**
* An annotation that indicates the version number since a member or a type has been present.
* This annotation is useful to manage versioning of your Json classes for a web-service.
* This annotation is useful to manage versioning of your JSON classes for a web-service.
*
* <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a
* {@link com.google.gson.GsonBuilder} and invoke
* {@link com.google.gson.GsonBuilder#setVersion(double)} method.
* {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
*
* <p>Here is an example of how this annotation is meant to be used:</p>
* <pre>
Expand All @@ -50,14 +50,16 @@
*
* @author Inderjeet Singh
* @author Joel Leitch
* @see GsonBuilder#setVersion(double)
* @see Until
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Since {
/**
* the value indicating a version number since this member
* or type has been present.
* The value indicating a version number since this member or type has been present.
* The number is inclusive; annotated elements will be included if {@code gsonVersion >= value}.
*/
double value();
}
18 changes: 10 additions & 8 deletions gson/src/main/java/com/google/gson/annotations/Until.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.gson.annotations;

import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -24,14 +25,13 @@

/**
* An annotation that indicates the version number until a member or a type should be present.
* Basically, if Gson is created with a version number that exceeds the value stored in the
* {@code Until} annotation then the field will be ignored from the JSON output. This annotation
* is useful to manage versioning of your JSON classes for a web-service.
* Basically, if Gson is created with a version number that is equal to or exceeds the value
* stored in the {@code Until} annotation then the field will be ignored from the JSON output.
* This annotation is useful to manage versioning of your JSON classes for a web-service.
*
* <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a
* {@link com.google.gson.GsonBuilder} and invoke
* {@link com.google.gson.GsonBuilder#setVersion(double)} method.
* {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
*
* <p>Here is an example of how this annotation is meant to be used:</p>
* <pre>
Expand All @@ -47,12 +47,14 @@
* methods will use all the fields for serialization and deserialization. However, if you created
* Gson with {@code Gson gson = new GsonBuilder().setVersion(1.2).create()} then the
* {@code toJson()} and {@code fromJson()} methods of Gson will exclude the {@code emailAddress}
* and {@code password} fields from the example above, because the version number passed to the
* and {@code password} fields from the example above, because the version number passed to the
* GsonBuilder, {@code 1.2}, exceeds the version number set on the {@code Until} annotation,
* {@code 1.1}, for those fields.
*
* @author Inderjeet Singh
* @author Joel Leitch
* @see GsonBuilder#setVersion(double)
* @see Since
* @since 1.3
*/
@Documented
Expand All @@ -61,8 +63,8 @@
public @interface Until {

/**
* the value indicating a version number until this member
* or type should be ignored.
* The value indicating a version number until this member or type should be be included.
* The number is exclusive; annotated elements will be included if {@code gsonVersion < value}.
*/
double value();
}
8 changes: 2 additions & 6 deletions gson/src/main/java/com/google/gson/internal/Excluder.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,19 +240,15 @@ private boolean isValidVersion(Since since, Until until) {
private boolean isValidSince(Since annotation) {
if (annotation != null) {
double annotationVersion = annotation.value();
if (annotationVersion > version) {
return false;
}
return version >= annotationVersion;
}
return true;
}

private boolean isValidUntil(Until annotation) {
if (annotation != null) {
double annotationVersion = annotation.value();
if (annotationVersion <= version) {
return false;
}
return version < annotationVersion;
}
return true;
}
Expand Down
33 changes: 31 additions & 2 deletions gson/src/test/java/com/google/gson/GsonBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@

package com.google.gson;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.fail;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import junit.framework.TestCase;
import org.junit.Test;

/**
* Unit tests for {@link GsonBuilder}.
*
* @author Inderjeet Singh
*/
public class GsonBuilderTest extends TestCase {
public class GsonBuilderTest {
private static final TypeAdapter<Object> NULL_TYPE_ADAPTER = new TypeAdapter<Object>() {
@Override public void write(JsonWriter out, Object value) {
throw new AssertionError();
Expand All @@ -39,6 +44,7 @@ public class GsonBuilderTest extends TestCase {
}
};

@Test
public void testCreatingMoreThanOnce() {
GsonBuilder builder = new GsonBuilder();
Gson gson = builder.create();
Expand All @@ -61,6 +67,7 @@ public void testCreatingMoreThanOnce() {
* Gson instances should not be affected by subsequent modification of GsonBuilder
* which created them.
*/
@Test
public void testModificationAfterCreate() {
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
Expand Down Expand Up @@ -136,6 +143,7 @@ public CustomClass3() {
}
}

@Test
public void testExcludeFieldsWithModifiers() {
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE)
Expand All @@ -151,6 +159,7 @@ static class HasModifiers {
String d = "d";
}

@Test
public void testTransientFieldExclusion() {
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers()
Expand All @@ -162,6 +171,7 @@ static class HasTransients {
transient String a = "a";
}

@Test
public void testRegisterTypeAdapterForCoreType() {
Type[] types = {
byte.class,
Expand All @@ -176,6 +186,7 @@ public void testRegisterTypeAdapterForCoreType() {
}
}

@Test
public void testDisableJdkUnsafe() {
Gson gson = new GsonBuilder()
.disableJdkUnsafe()
Expand All @@ -198,4 +209,22 @@ private static class ClassWithoutNoArgsConstructor {
public ClassWithoutNoArgsConstructor(String s) {
}
}

@Test
public void testSetVersionInvalid() {
GsonBuilder builder = new GsonBuilder();
try {
builder.setVersion(Double.NaN);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid version: NaN", e.getMessage());
}

try {
builder.setVersion(-0.1);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid version: -0.1", e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,82 @@

package com.google.gson;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
import com.google.gson.internal.Excluder;
import junit.framework.TestCase;
import org.junit.Test;

/**
* Unit tests for the {@link Excluder} class.
*
* @author Joel Leitch
*/
public class VersionExclusionStrategyTest extends TestCase {
public class VersionExclusionStrategyTest {
private static final double VERSION = 5.0D;

public void testClassAndFieldAreAtSameVersion() throws Exception {
@Test
public void testSameVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION);
assertFalse(excluder.excludeClass(MockObject.class, true));
assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
assertFalse(excluder.excludeClass(MockClassSince.class, true));
assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));

// Until version is exclusive
assertTrue(excluder.excludeClass(MockClassUntil.class, true));
assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));

assertFalse(excluder.excludeClass(MockClassBoth.class, true));
assertFalse(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
}

public void testClassAndFieldAreBehindInVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 1);
assertFalse(excluder.excludeClass(MockObject.class, true));
assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
@Test
public void testNewerVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5);
assertFalse(excluder.excludeClass(MockClassSince.class, true));
assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));

assertTrue(excluder.excludeClass(MockClassUntil.class, true));
assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));

assertTrue(excluder.excludeClass(MockClassBoth.class, true));
assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
}

@Test
public void testOlderVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5);
assertTrue(excluder.excludeClass(MockClassSince.class, true));
assertTrue(excluder.excludeField(MockClassSince.class.getField("someField"), true));

assertFalse(excluder.excludeClass(MockClassUntil.class, true));
assertFalse(excluder.excludeField(MockClassUntil.class.getField("someField"), true));

assertTrue(excluder.excludeClass(MockClassBoth.class, true));
assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
}

public void testClassAndFieldAreAheadInVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 1);
assertTrue(excluder.excludeClass(MockObject.class, true));
assertTrue(excluder.excludeField(MockObject.class.getField("someField"), true));
@Since(VERSION)
private static class MockClassSince {

@Since(VERSION)
public final int someField = 0;
}

@Until(VERSION)
private static class MockClassUntil {

@Until(VERSION)
public final int someField = 0;
}

@Since(VERSION)
private static class MockObject {
@Until(VERSION + 2)
private static class MockClassBoth {

@Since(VERSION)
@Until(VERSION + 2)
public final int someField = 0;
}
}
Loading

0 comments on commit 796193d

Please sign in to comment.