// 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; } }