// 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* * {@link RestrictJsonNames} * ------------------------- * *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"] * } *
* - 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* * {@link JsonableElement} with "*" for name - merge instead of sub-object * ----------------------------------------------------------------------- * *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) *
* 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