com.palantir.ptoss.cinch.core.BindingContext.java Source code

Java tutorial

Introduction

Here is the source code for com.palantir.ptoss.cinch.core.BindingContext.java

Source

//   Copyright 2011 Palantir Technologies
//
//   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.palantir.ptoss.cinch.core;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.Validate;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.palantir.ptoss.cinch.swing.Bound;
import com.palantir.ptoss.cinch.swing.OnChange;
import com.palantir.ptoss.util.Reflections;

/**
 * <p>A {@link BindingContext} holds information about how to bind various parts of a Java Object.
 *
 * <p><b>Binding Constants</b> - TODO
 *
 * <p><b>Visibility</b> - TODO
 *
 * <p><b>Subclassing</b> - TODO
 *
 * <p><b>Bindable models</b> - have to be final
 */
public class BindingContext {
    /**
     * The object for which the context has been built.
     */
    private final Object object;

    /**
     * All of the fields of type {@link BindableModel} on the object.
     * @see #indexBindableModels()
     */
    private final Map<String, Field> bindableModels;

    /**
     * All of the bindable methods on the object.
     * @see #indexBindableMethods()
     */
    private final Map<String, ObjectFieldMethod> bindableMethods;

    /**
     * @see #indexBindableModelMethods()
     */
    private final Map<String, ObjectFieldMethod> bindableModelMethods;

    /**
     * The map from all static final field names on the object to the objects contained in those
     * fields.
     * @see #indexBindableConstants()
     */
    private final Map<String, Object> bindableConstants;

    /**
     * All of the getters available on the bindable models.
     */
    private final Map<String, ObjectFieldMethod> bindableGetters;

    /**
     * All of the setters available on the bindable models.
     */
    private final Map<String, ObjectFieldMethod> bindableSetters;

    /**
     * Create a BindingContext for the given, non-null object.  Throws a {@link BindingException}
     * if there is a problem.
     * @param object the object - cannot be null
     */
    public BindingContext(Object object) {
        Validate.notNull(object);
        this.object = object;
        try {
            bindableModels = indexBindableModels();
            bindableMethods = indexBindableMethods();
            bindableModelMethods = indexBindableModelMethods();
            bindableConstants = indexBindableConstants();
            bindableGetters = indexBindableProperties(
                    Reflections.getterFunction(PropertyDescriptor.class, Method.class, "readMethod"));
            bindableSetters = indexBindableProperties(
                    Reflections.getterFunction(PropertyDescriptor.class, Method.class, "writeMethod"));
        } catch (Exception e) {
            throw new BindingException("could not create BindingContext", e);
        }
    }

    /**
     * Gets a constant from the binding context.  Constants are static, final fields of the bound
     * object.
     *
     * @param key the name of the field
     * @return the value of the field
     */
    public Object getBindableConstant(String key) {
        return bindableConstants.get(key);
    }

    /**
     * Look through all of the declared, static, final fields of the context object, grab the value,
     * and insert a mapping from the field's name to the object.
     *
     * Note that this will index non-public fields.
     *
     * @return the bindable constants map
     * @throws IllegalArgumentException on reflection error
     * @throws IllegalAccessException on reflection error
     */
    private Map<String, Object> indexBindableConstants() throws IllegalArgumentException, IllegalAccessException {
        Map<String, Object> map = Maps.newHashMap();
        for (Field field : object.getClass().getDeclaredFields()) {
            boolean accessible = field.isAccessible();
            field.setAccessible(true);
            if (Reflections.isFieldFinal(field) && Reflections.isFieldStatic(field)) {
                map.put(field.getName(), field.get(object));
            }
            field.setAccessible(accessible);
        }
        return map;
    }

    /**
     * Returns the value of the specified Field on the object bound by this {@link BindingContext}
     *
     * @param field {@link Field} to pull the value from
     * @param klass return type of value in the {@link Field}
     * @return value of type <code>klass</code> from field <code>field</code> on bound object.
     * @throws IllegalArgumentException if the passed {@link Field} is not a field on the object
     * bound by this {@link BindingContext}
     */
    public <T> T getFieldObject(Field field, Class<T> klass) throws IllegalArgumentException {
        return Reflections.getFieldObject(object, field, klass);
    }

    /**
     * Looks up an {@link ObjectFieldMethod} tuple by its key.
     * @param key - generated by {@link OnChange#call()}
     * @return the tuple for this key (or null, if it doesn't exist)
     */
    public ObjectFieldMethod getBindableMethod(String key) {
        ObjectFieldMethod ofm = bindableMethods.get(key);
        return ofm;
    }

    /**
     * Looks up an {@link ObjectFieldMethod} tuple by its key.
     * @param key - generated by {@link OnChange#call()}
     * @return the tuple for this key (or null, if it doesn't exist)
     */
    // TODO (regs) dead code?
    public ObjectFieldMethod getBindableModelMethod(String key) {
        ObjectFieldMethod ofm = bindableModelMethods.get(key);
        return ofm;
    }

    public BindableModel getBindableModel(String key) {
        Field field = bindableModels.get(key);
        if (field == null) {
            return null;
        }
        return getFieldObject(field, BindableModel.class);
    }

    public Object evalOnObject(String on, BindableModel model) {
        return findOnObject(on, model);
    }

    /**
     * Returns the list of {@link ModelUpdate} types in this binding context.
     * @param modelClass
     * @return the of {@link Class}es that implement {@link ModelUpdate} in this binding context.
     */
    public static List<Class<?>> findModelUpdateClass(final BindableModel modelClass) {
        List<Class<?>> classes = Reflections.getTypesOfTypeForClassHierarchy(modelClass.getClass(),
                ModelUpdate.class);
        Predicate<Class<?>> isEnum = new Predicate<Class<?>>() {
            public boolean apply(final Class<?> input) {
                return input.isEnum();
            }
        };
        // Look for ModelUpdate classes in implemented interfaces
        classes = Lists.newArrayList(Iterables.filter(classes, isEnum));
        for (Class<?> iface : modelClass.getClass().getInterfaces()) {
            classes.addAll(Lists.newArrayList(Iterables
                    .filter(Reflections.getTypesOfTypeForClassHierarchy(iface, ModelUpdate.class), isEnum)));
        }
        if (classes.size() == 0) {
            return null;
        }
        return classes;
    }

    /**
     * Resolves a string reference, as specified in the <code>on</code> parameter of
     * a {@link Bound} annotation to an Enum object in this runtime.
     * @param on <code>on</code> parameter from a {@link Bound} annotation.
     * @param model
     * @return the resolved object
     * @throws IllegalArgumentException if the referenced object can't be found.
     */
    public static ModelUpdate findOnObject(final String on, final BindableModel model) {
        ModelUpdate onObject = null;
        if (on != null && on.trim().length() > 0) {
            final List<Class<?>> updateClasses = findModelUpdateClass(model);
            for (Class<?> updateClass : updateClasses) {
                try {
                    onObject = (ModelUpdate) Reflections.evalEnum(updateClass, on);
                    return onObject;
                } catch (IllegalArgumentException e) {
                    // swallow this if we don't find the enum on one of the
                    // classes, continue to next class.
                }
            }
            throw new IllegalArgumentException("could not find \"on\" parameter " + on);
        }
        return onObject;
    }

    private Map<String, ObjectFieldMethod> indexBindableProperties(Function<PropertyDescriptor, Method> methodFn)
            throws IntrospectionException {
        final Map<ObjectFieldMethod, String> getterOfms = Maps.newHashMap();
        for (Field field : Sets.newHashSet(bindableModels.values())) {
            BeanInfo beanInfo = Introspector.getBeanInfo(field.getType());
            PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor descriptor : props) {
                Method method = methodFn.apply(descriptor);
                if (method == null) {
                    continue;
                }
                BindableModel model = getFieldObject(field, BindableModel.class);
                getterOfms.put(new ObjectFieldMethod(model, field, method), descriptor.getName());
            }
        }
        return dotIndex(getterOfms.keySet(), ObjectFieldMethod.TO_FIELD_NAME, Functions.forMap(getterOfms));
    }

    private static <T> Map<String, T> dotIndex(Collection<T> items, Function<T, String> qualifierFn,
            Function<T, String> blindFn) {
        Set<String> ambiguousNames = Sets.newHashSet();
        Map<String, T> results = Maps.newHashMap();
        for (T item : items) {
            String blindKey = blindFn.apply(item);
            if (!ambiguousNames.contains(blindKey)) {
                if (results.containsKey(blindKey)) {
                    results.remove(blindKey);
                    ambiguousNames.add(blindKey);
                } else {
                    results.put(blindKey, item);
                }
            }
            String qualifiedKey = qualifierFn.apply(item) + "." + blindKey;
            results.put(qualifiedKey, item);
        }
        return results;
    }

    public ObjectFieldMethod findGetter(String property) {
        return bindableGetters.get(property);
    }

    public ObjectFieldMethod findSetter(String property) {
        return bindableSetters.get(property);
    }

    public Set<BindableModel> getBindableModels() {
        Function<Field, BindableModel> f = new Function<Field, BindableModel>() {
            public BindableModel apply(Field from) {
                return getFieldObject(from, BindableModel.class);
            }
        };
        return ImmutableSet.copyOf(Iterables.transform(bindableModels.values(), f));
    }

    public List<Field> getAnnotatedFields(Class<? extends Annotation> klass) {
        return Reflections.getAnnotatedFieldsForClassHierarchy(object.getClass(), klass);
    }

    public List<ObjectFieldMethod> getAnnotatedParameterlessMethods(final Class<? extends Annotation> annotation) {
        return Lists.newArrayList(Iterables.filter(Reflections.getParameterlessMethodsForClassHierarchy(object),
                new Predicate<ObjectFieldMethod>() {
                    public boolean apply(ObjectFieldMethod input) {
                        return input.getMethod().isAnnotationPresent(annotation);
                    }
                }));
    }

    private static List<ObjectFieldMethod> getParameterlessMethods(Object object, Field field) {
        List<ObjectFieldMethod> methods = Lists.newArrayList();
        for (Method method : field.getType().getDeclaredMethods()) {
            if (method.getParameterTypes().length == 0 && Reflections.isMethodPublic(method)) {
                methods.add(new ObjectFieldMethod(object, field, method));
            }
        }
        return methods;
    }

    private static List<ObjectFieldMethod> getParameterlessMethodsOnFieldTypes(Object object, List<Field> fields)
            throws IllegalArgumentException {
        List<ObjectFieldMethod> methods = Lists.newArrayList();
        for (Field field : fields) {
            Object fieldObject = Reflections.getFieldObject(object, field, Object.class);
            methods.addAll(getParameterlessMethods(fieldObject, field));
        }
        return methods;
    }

    private static Map<String, ObjectFieldMethod> indexMethods(List<ObjectFieldMethod> methods)
            throws IllegalArgumentException {
        Map<String, ObjectFieldMethod> map = Maps.newHashMap();
        Set<String> ambiguousNames = Sets.newHashSet();
        for (ObjectFieldMethod ofm : methods) {
            Method method = ofm.getMethod();
            String blindKey = method.getName();
            if (!ambiguousNames.contains(blindKey)) {
                if (map.containsKey(blindKey)) {
                    map.remove(blindKey);
                    ambiguousNames.add(blindKey);
                } else {
                    map.put(blindKey, ofm);
                }
            }
            String fieldName = ofm.getField() == null ? "this" : ofm.getField().getName();
            String qualifiedKey = fieldName + "." + blindKey;
            map.put(qualifiedKey, ofm);
        }
        return map;
    }

    private List<Field> getBindableModelFields() {
        List<Field> allModelFields = Reflections.getFieldsOfTypeForClassHierarchy(object.getClass(),
                BindableModel.class);
        List<Field> notBindableFields = Reflections.getAnnotatedFieldsForClassHierarchy(object.getClass(),
                NotBindable.class);
        allModelFields = ImmutableList
                .copyOf(Iterables.filter(allModelFields, Predicates.not(Predicates.in(notBindableFields))));
        List<Field> nonFinalModelFields = ImmutableList
                .copyOf(Iterables.filter(allModelFields, Predicates.not(Reflections.IS_FIELD_FINAL)));
        if (!nonFinalModelFields.isEmpty()) {
            throw new BindingException("All BindableModels have to be final or marked with @NotBindable, but "
                    + Iterables.transform(nonFinalModelFields, Reflections.FIELD_TO_NAME) + " are not.");
        }
        return allModelFields;
    }

    /**
     * Indexes all bindable models within the binding context. If there are two bindable models
     * in a class hierarchy with identical names then they are indexed as
     * "DeclaringClass.modelFieldName". If this is not unique then one of them will win
     * non-deterministically, don't do this.
     * @return the index
     */
    private Map<String, Field> indexBindableModels() {
        return dotIndex(getBindableModelFields(), Reflections.FIELD_TO_CONTAINING_CLASS_NAME,
                Reflections.FIELD_TO_NAME);
    }

    /*
     * TODO Current behavior is if ANY class in a class hierarchy is Bindable then all methods in that
     * hierarchy are bindable.  Really this should be for each class in the hierarchy, if it's
     * marked Bindable then its methods are bindable.
     */
    private Map<String, ObjectFieldMethod> indexBindableMethods() throws IllegalArgumentException {
        // Get all fields marked @Bindable
        List<Field> bindables = getAnnotatedFields(Bindable.class);
        if (Iterables.any(bindables, Predicates.not(Reflections.IS_FIELD_FINAL))) {
            throw new BindingException("all @Bindables have to be final");
        }
        // Add all BindableModels
        bindables.addAll(getBindableModelFields());

        // Index those methods.
        List<ObjectFieldMethod> methods = getParameterlessMethodsOnFieldTypes(object, bindables);

        // Add methods for classes marked @Bindable
        if (Reflections.isClassAnnotatedForClassHierarchy(object, Bindable.class)) {
            methods.addAll(Reflections.getParameterlessMethodsForClassHierarchy(object));
        }

        return indexMethods(methods);
    }

    private Map<String, ObjectFieldMethod> indexBindableModelMethods() throws IllegalArgumentException {
        List<ObjectFieldMethod> methods = getParameterlessMethodsOnFieldTypes(object, getBindableModelFields());
        return indexMethods(methods);
    }

    static <T> boolean isOn(Object onObject, Set<T> changedSet) {
        if (changedSet.contains(ModelUpdates.ALL)) {
            return true;
        }
        return changedSet.contains(onObject);
    }

    public static <T extends Enum<?> & ModelUpdate> boolean isOn(Object onObject, T... changed) {
        if (onObject == null) {
            return true;
        }
        final Set<T> changedSet = Sets.newHashSet(changed);
        return BindingContext.isOn(onObject, changedSet);
    }

    public static <T extends Enum<?> & ModelUpdate> boolean isOn(Collection<Object> ons, T... changed) {
        if (ons == null || ons.isEmpty()) {
            return true;
        }
        final Set<T> changedSet = Sets.newHashSet(changed);
        for (Object on : ons) {
            if (BindingContext.isOn(on, changedSet)) {
                return true;
            }
        }
        return false;
    }

    public static List<Object> getOnObjects(String[] ons, BindableModel model) {
        if (ons == null) {
            return null;
        }
        List<Object> onObjects = Lists.newArrayList();
        for (int i = 0; i < ons.length; i++) {
            Object onObject = findOnObject(ons[i], model);
            if (onObject != null) {
                onObjects.add(onObject);
            }
        }
        return onObjects;
    }
}