mirror of
https://github.com/google/nomulus.git
synced 2025-04-29 19:47:51 +02:00
293 lines
13 KiB
Java
293 lines
13 KiB
Java
// Copyright 2016 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.Preconditions.checkNotNull;
|
|
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 static java.util.Arrays.asList;
|
|
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
import com.google.common.base.Function;
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.Preconditions;
|
|
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.Maps;
|
|
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<Class<?>, ImmutableMap<String, Field>> ALL_FIELDS_CACHE =
|
|
CacheBuilder.newBuilder().build(new CacheLoader<Class<?>, ImmutableMap<String, Field>>() {
|
|
@Override
|
|
public ImmutableMap<String, Field> load(Class<?> clazz) {
|
|
Deque<Class<?>> 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<String, Field> fields = new LinkedHashMap<>();
|
|
for (Class<?> hierarchyClass : hierarchy) {
|
|
Package pakkage = hierarchyClass.getPackage();
|
|
// Don't use hierarchyClass.getFields() because it only picks up public fields.
|
|
for (Field field : hierarchyClass.getDeclaredFields()) {
|
|
if (Modifier.isStatic(field.getModifiers())) {
|
|
continue;
|
|
}
|
|
// Strictly speaking this shouldn't be necessary since all of these fields
|
|
// are already accessible to their FieldExposer, but it is more performant
|
|
// to access fields if they are marked accessible this way because it skips
|
|
// various security checks.
|
|
checkNotNull(
|
|
FIELD_EXPOSERS.get(pakkage),
|
|
"No FieldExposer registered for %s", pakkage.getName())
|
|
.setAccessible(field);
|
|
fields.put(field.getName(), field);
|
|
}
|
|
}
|
|
return ImmutableMap.copyOf(fields);
|
|
}});
|
|
|
|
/** Per-package trampolines to expose package-private fields for reflection. */
|
|
private static final Map<Package, AbstractFieldExposer> FIELD_EXPOSERS = Maps.uniqueIndex(
|
|
FieldExposerRegistry.getFieldExposers(),
|
|
new Function<AbstractFieldExposer, Package>() {
|
|
@Override
|
|
public Package apply(AbstractFieldExposer exposer) {
|
|
return exposer.getClass().getPackage();
|
|
}});
|
|
|
|
/** Lists all instance fields on an object, including non-public and inherited fields. */
|
|
static Map<String, Field> 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(asList(clazz.getEnumConstants()));
|
|
} else {
|
|
stringBuilder.append("class ");
|
|
body = FluentIterable.from(getAllFields(clazz).values())
|
|
.filter(new Predicate<Field>() {
|
|
@Override
|
|
public boolean apply(Field field) {
|
|
return !field.isAnnotationPresent(Ignore.class);
|
|
}})
|
|
.transform(new Function<Field, Object>() {
|
|
@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<Class<?>> getPersistedFieldTypes(Class<?> clazz) {
|
|
ImmutableSet.Builder<Class<?>> 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 Preconditions.checkNotNull(
|
|
FIELD_EXPOSERS.get(field.getDeclaringClass().getPackage()),
|
|
"No FieldExposer registered for %s", field.getDeclaringClass().getPackage().getName())
|
|
.getFieldValue(instance, field);
|
|
} catch (IllegalAccessException e) {
|
|
throw new IllegalStateException(e);
|
|
}
|
|
}
|
|
|
|
/** Sets a field value via reflection. */
|
|
static void setFieldValue(Object instance, Field field, Object value) {
|
|
try {
|
|
Preconditions.checkNotNull(
|
|
FIELD_EXPOSERS.get(field.getDeclaringClass().getPackage()),
|
|
"No FieldExposer registered for %s", field.getDeclaringClass().getPackage().getName())
|
|
.setFieldValue(instance, field, value);
|
|
} catch (IllegalAccessException e) {
|
|
throw new IllegalStateException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a map from Field objects (including non-public and inherited fields) to values.
|
|
*
|
|
* <p>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<Field, Object> getFieldValues(Object instance) {
|
|
// Don't make this ImmutableMap because field values can be null.
|
|
Map<Field, Object> 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<Object>() {
|
|
@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<Object, ?> CLONE_EMPTY_TO_NULL = new Function<Object, Object>() {
|
|
@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<Object> 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 extends ImmutableObject> T cloneEmptyToNull(T obj) {
|
|
return (T) CLONE_EMPTY_TO_NULL.apply(obj);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static void resetCaches() {
|
|
ALL_FIELDS_CACHE.invalidateAll();
|
|
}
|
|
}
|