google-nomulus/java/com/google/domain/registry/model/ModelUtils.java
2016-03-01 17:59:16 -05:00

296 lines
13 KiB
Java

// Copyright 2016 Google Inc. 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 com.google.domain.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.Ref;
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 and Ref objects, since they are just references to other objects and don't
// actual embed themselves in the persisted object anyway.
Class<?> fieldClazz = field.getType();
Type fieldType = field.getGenericType();
builder.add(fieldClazz);
if (fieldType.equals(fieldClazz) || Ref.class.equals(clazz) || 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 names (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<String, Object> getFieldValues(Object instance) {
// Don't make this ImmutableMap because field values can be null.
Map<String, 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.getName(), 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();
}
}