// Copyright 2019 The Nomulus Authors. All Rights Reserved. // // Licensed 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 google.registry.rdap; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.joda.time.DateTime; /** * An Jsonable that can turn itself into a JSON object using reflection. * *
This can only be used to create JSON *objects*, so if your class needs a different JSON type, * you'll have to implement Jsonable yourself. (for example, VCards objects are represented as a * list rather than an object) * *
You can annotate fields or methods with 0 parameters with {@link JsonElement}, and its value * will be "JSONified" and added to the generated JSON object. * *
This implementation is geared towards RDAP replies, and hence has RDAP-specific quirks. * Specifically: * * - Fields with empty arrays are not shown at all * * - VCards are a built-in special case (Not implemented yet) * * - DateTime conversion is specifically supported as if it were a primitive * * - Arrays are considered to be SETS rather than lists, meaning repeated values are removed and the * order isn't guaranteed * * Usage: * * {@link JsonableElement} * ----------------------- * *
* - JsonableElement annotates Members that become JSON object fields:
*
* class Something extends AbstractJsonableObject {
* @JsonableElement public String a = "value1";
* @JsonableElement public String b() {return "value2";}
* }
*
* will result in:
* {
* "a": "value1",
* "b": "value2"
* }
*
* - Passing a name to JsonableElement overrides the Member's name. Multiple elements with the same
* name is an error (except for lists - see later)
*
* class Something extends AbstractJsonableObject {
* @JsonableElement("b") public String a = "value1";
* }
*
* will result in:
* {
* "b": "value1"
* }
*
* - the supported object types are String, Boolean, Number, DateTime, Jsonable. In addition,
* Iterable and Optional are respected.
*
* - An Optional that's empty is skipped, while a present Optional acts exactly like the object it
* wraps. Null values are errors.
*
* class Something extends AbstractJsonableObject {
* @JsonableElement public Optional a = Optional.of("value1");
* @JsonableElement public Optional b = Optional.empty();
* }
*
* will result in:
* {
* "a": "value1"
* }
*
* - An Iterable will turn into an array. Multiple Iterables with the same name are merged. Remember
* - arrays are treated as "sets" here.
*
* class Something extends AbstractJsonableObject {
* @JsonableElement("lst") public List a = ImmutableList.of("value1", "value2");
* @JsonableElement("lst") public List b = ImmutableList.of("value2", "value3");
* }
*
* will result in:
* {
* "lst": ["value1", "value2", "value3"]
* }
*
* - A single element with a [] after its name is added to the array as if it were wrapped in a list
* with one element. Optionals are still respected (an empty Optional is skipped).
*
* class Something extends AbstractJsonableObject {
* @JsonableElement("lst") public List a = ImmutableList.of("value1", "value2");
* @JsonableElement("lst[]") public String b = "value3";
* @JsonableElement("lst[]") public Optional c = Optional.empty();
* }
*
* will result in:
* {
* "lst": ["value1", "value2", "value3"]
* }
*
*
* {@link RestrictJsonNames}
* -------------------------
*
*
* - RestrictJsonNames is a way to prevent typos in the JsonableElement names.
*
* - If it annotates a Jsonable class declaration, this class can only be annotated with
* JsonableElements with one of the allowed names
*
* @RestrictJsonNames({"key", "lst[]"})
* class Something implements Jsonable {...}
*
* means that Something can only be used as an element named "key", OR as an element in an array
* named "lst".
*
* @JsonableElement public Something something; // ERROR
* @JsonableElement("something") public Something key; // ERROR
* @JsonableElement public Something key; // OK
* @JsonableElement("key") public Something something; // OK
* @JsonableElement("lst") public List myList; // OK
* @JsonableElement("lst[]") public Something something; // OK
*
* - @RestrictJsonNames({}) means this Jsonable can't be inserted into an AbstractJsonableObject at
* all. It's useful for "outer" Jsonable that are to be returned as is, or for
* AbstractJsonableObject only used for JsonableElement("*") (Merging - see next)
*
*
* {@link JsonableElement} with "*" for name - merge instead of sub-object
* -----------------------------------------------------------------------
*
*
* The special name "*" means we want to merge the object with the current object instead of having
* it a value for the name key.
*
* THIS MIGHT BE REMOVED LATER, we'll see how it goes. Currently it's only used in one place and
* might not be worth it.
*
* - JsonableElement("*") annotates an AbstractJsonableObject Member that is to be merged with the
* current AbstractJsonableObject, instead of being a sub-object
*
* class Something extends AbstractJsonableObject {
* @JsonableElement public String a = "value1";
* }
*
* class Other extends AbstractJsonableObject {
* @JsonableElement("*") public Something something = new Something();
* @JsonableElement public String b = "value2";
* }
*
* Other will result in:
* {
* "a": "value1",
* "b": "value2"
* }
*
* - Arrays of the same name are merges (remember, they are considered sets so duplicates are
* removed), but elements with the same name are an error
*
* class Something extends AbstractJsonableObject {
* @JsonableElement("lst[]") public String a = "value1";
* }
*
* class Other extends AbstractJsonableObject {
* @JsonableElement("*") public Something something = new Something();
* @JsonableElement("lst[]") public String b = "value2";
* }
*
* Other will result in:
* {
* "lst": ["value1", "value2"]
* }
*
* - Optionals are still respected. An empty JsonableElement("*") is skipped.
*
*/
@SuppressWarnings("InvalidBlockTag")
abstract class AbstractJsonableObject implements Jsonable {
private static final String ARRAY_NAME_SUFFIX = "[]";
private static final String MERGE_NAME = "*";
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RUNTIME)
@interface JsonableElement {
String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RUNTIME)
@interface RestrictJsonNames {
String[] value();
}
@Override
public final JsonObject toJson() {
try {
JsonObjectBuilder builder = new JsonObjectBuilder();
for (Field field : getAllJsonableElementFields()) {
JsonableElement jsonableElement = field.getAnnotation(JsonableElement.class);
Object object;
try {
field.setAccessible(true);
object = field.get(this);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
String.format("Error reading value of field '%s'", field), e);
} finally {
field.setAccessible(false);
}
builder.add(jsonableElement, field, object);
}
for (Method method : getAllJsonableElementMethods()) {
JsonableElement jsonableElement = method.getAnnotation(JsonableElement.class);
Object object;
try {
method.setAccessible(true);
object = method.invoke(this);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(
String.format("Error reading value of method '%s'", method), e);
} finally {
method.setAccessible(false);
}
builder.add(jsonableElement, method, object);
}
return builder.build();
} catch (Throwable e) {
throw new JsonableException(
e, String.format("Error JSONifying %s: %s", this.getClass(), e.getMessage()));
}
}
/**
* Get all the fields declared on this class.
*
* We aren't using {@link Class#getFields} because that would return only the public fields.
*/
private Iterable We aren't using {@link Class#getMethods} because that would return only the public methods.
*/
private Iterable Empty means there are no restrictions - all names are allowed.
*
* If not empty - the resulting list is the allowed names. If the name ends with [], it means
* the class is an element in a array with this name.
*
* A name of "*" means this is allowed to merge.
*/
static Optional A name is not allowed if the object's class (or one of its ancesstors) is annotated
* with @RestrictJsonNames and the name isn't in that list.
*
* If there's no @RestrictJsonNames annotation, all names are allowed.
*/
static void verifyAllowedJsonKeyName(String name, @Nullable Member member, Class> clazz) {
Optional