// Copyright 2017 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.model; import static com.google.common.base.Predicates.instanceOf; import static com.google.common.base.Predicates.isNull; import static com.google.common.base.Predicates.or; import static com.google.common.collect.Iterables.all; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.transformValues; import static com.google.common.collect.Sets.newLinkedHashSet; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Ordering; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Ignore; import com.googlecode.objectify.annotation.Parent; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.AbstractList; import java.util.Collection; import java.util.Deque; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** A collection of static methods that deal with reflection on model classes. */ public class ModelUtils { /** Caches all instance fields on an object, including non-public and inherited fields. */ private static final LoadingCache, ImmutableMap> ALL_FIELDS_CACHE = CacheBuilder.newBuilder().build(new CacheLoader, ImmutableMap>() { @Override public ImmutableMap load(Class clazz) { Deque> hierarchy = new LinkedList<>(); // Walk the hierarchy up to but not including ImmutableObject (to ignore hashCode). for (; clazz != ImmutableObject.class; clazz = clazz.getSuperclass()) { // Add to the front, so that shadowed fields show up later in the list. // This will mean that getFieldValues will show the most derived value. hierarchy.addFirst(clazz); } Map fields = new LinkedHashMap<>(); for (Class hierarchyClass : hierarchy) { // Don't use hierarchyClass.getFields() because it only picks up public fields. for (Field field : hierarchyClass.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers())) { field.setAccessible(true); fields.put(field.getName(), field); } } } return ImmutableMap.copyOf(fields); }}); /** Lists all instance fields on an object, including non-public and inherited fields. */ static Map getAllFields(Class clazz) { return ALL_FIELDS_CACHE.getUnchecked(clazz); } /** Return a string representing the persisted schema of a type or enum. */ static String getSchema(Class clazz) { StringBuilder stringBuilder = new StringBuilder(); Iterable body; if (clazz.isEnum()) { stringBuilder.append("enum "); body = FluentIterable.from(clazz.getEnumConstants()); } else { stringBuilder.append("class "); body = FluentIterable.from(getAllFields(clazz).values()) .filter(new Predicate() { @Override public boolean apply(Field field) { return !field.isAnnotationPresent(Ignore.class); }}) .transform(new Function() { @Override public Object apply(Field field) { String annotation = field.isAnnotationPresent(Id.class) ? "@Id " : field.isAnnotationPresent(Parent.class) ? "@Parent " : ""; String type = field.getType().isArray() ? field.getType().getComponentType().getName() + "[]" : field.getGenericType().toString().replaceFirst("class ", ""); return String.format("%s%s %s", annotation, type, field.getName()); }}); } return stringBuilder .append(clazz.getName()).append(" {\n ") .append(Joiner.on(";\n ").join(Ordering.usingToString().sortedCopy(body))) .append(";\n}") .toString(); } /** * Returns the set of Class objects of all persisted fields. This includes the parameterized * type(s) of any fields (if any). */ static Set> getPersistedFieldTypes(Class clazz) { ImmutableSet.Builder> builder = new ImmutableSet.Builder<>(); for (Field field : getAllFields(clazz).values()) { // Skip fields that aren't persisted to Datastore. if (field.isAnnotationPresent(Ignore.class)) { continue; } // If the field's type is the same as the field's class object, then it's a non-parameterized // type, and thus we just add it directly. We also don't bother looking at the parameterized // types of Key objects, since they are just references to other objects and don't actually // embed themselves in the persisted object anyway. Class fieldClazz = field.getType(); Type fieldType = field.getGenericType(); builder.add(fieldClazz); if (fieldType.equals(fieldClazz) || Key.class.equals(clazz)) { continue; } // If the field is a parameterized type, then also add the parameterized field. if (fieldType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) fieldType; for (Type actualType : parameterizedType.getActualTypeArguments()) { if (actualType instanceof Class) { builder.add((Class) actualType); } else { // We intentionally ignore types that are parameterized on non-concrete types. In theory // we could have collections embedded within collections, but Objectify does not allow // that. } } } } return builder.build(); } /** Retrieves a field value via reflection. */ static Object getFieldValue(Object instance, Field field) { try { return field.get(instance); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } } /** Sets a field value via reflection. */ static void setFieldValue(Object instance, Field field, Object value) { try { field.set(instance, value); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } } /** * Returns a map from Field objects (including non-public and inherited fields) to values. * *

This turns arrays into {@link List} objects so that ImmutableObject can more easily use the * returned map in its implementation of {@link ImmutableObject#toString} and {@link * ImmutableObject#equals}, which work by comparing and printing these maps. */ static Map getFieldValues(Object instance) { // Don't make this ImmutableMap because field values can be null. Map values = new LinkedHashMap<>(); for (Field field : getAllFields(instance.getClass()).values()) { Object value = getFieldValue(instance, field); if (value != null && value.getClass().isArray()) { // It's surprisingly difficult to convert arrays into lists if the array might be primitive. final Object arrayValue = value; value = new AbstractList() { @Override public Object get(int index) { return Array.get(arrayValue, index); } @Override public int size() { return Array.getLength(arrayValue); }}; } values.put(field, value); } return values; } /** Functional helper for {@link #cloneEmptyToNull}. */ private static final Function CLONE_EMPTY_TO_NULL = new Function() { @Override public Object apply(Object obj) { if (obj instanceof ImmutableSortedMap) { // ImmutableSortedMapTranslatorFactory handles empty for us. If the object is null, then // its on-save hook can't run. return obj; } if ("".equals(obj) || (obj instanceof Collection && ((Collection) obj).isEmpty()) || (obj instanceof Map && ((Map) obj).isEmpty()) || (obj != null && obj.getClass().isArray() && Array.getLength(obj) == 0)) { return null; } Predicate immutableObjectOrNull = or(isNull(), instanceOf(ImmutableObject.class)); if ((obj instanceof Set || obj instanceof List) && all((Iterable) obj, immutableObjectOrNull)) { // Recurse into sets and lists, but only if they contain ImmutableObjects. FluentIterable fluent = FluentIterable.from((Iterable) obj).transform(this); return (obj instanceof List) ? newArrayList(fluent) : newLinkedHashSet(fluent); } if (obj instanceof Map && all(((Map) obj).values(), immutableObjectOrNull)) { // Recurse into maps with ImmutableObject values. return transformValues((Map) obj, this); } if (obj instanceof ImmutableObject) { // Recurse on the fields of an ImmutableObject. ImmutableObject copy = ImmutableObject.clone((ImmutableObject) obj); for (Field field : getAllFields(obj.getClass()).values()) { Object oldValue = getFieldValue(obj, field); Object newValue = apply(oldValue); if (!Objects.equals(oldValue, newValue)) { setFieldValue(copy, field, newValue); } } return copy; } return obj; }}; /** Returns a clone of the object and sets empty collections, arrays, maps and strings to null. */ @SuppressWarnings("unchecked") protected static T cloneEmptyToNull(T obj) { return (T) CLONE_EMPTY_TO_NULL.apply(obj); } @VisibleForTesting static void resetCaches() { ALL_FIELDS_CACHE.invalidateAll(); } }