google.registry.model.ModelUtils.java Source code

Java tutorial

Introduction

Here is the source code for google.registry.model.ModelUtils.java

Source

// 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.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.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<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) {
                        // 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<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 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.
     *
     * <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();
    }
}