/* * Beanfabrics Framework Copyright (C) by Michael Karneim, beanfabrics.org * Use is subject to license terms. See license.txt. */ // TODO javadoc - remove this comment only when the class and all non-public // methods and fields are documented package org.beanfabrics.support; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.beanfabrics.Path; import org.beanfabrics.PathEvaluation; import org.beanfabrics.context.ContextOwner; import org.beanfabrics.log.Logger; import org.beanfabrics.log.LoggerFactory; import org.beanfabrics.model.IOperationPM; import org.beanfabrics.model.PresentationModel; import org.beanfabrics.model.PresentationModelFilter; import org.beanfabrics.model.PresentationModelVisitor; import org.beanfabrics.util.FilteredModels; import org.beanfabrics.util.ReflectionUtil; /** * @author Michael Karneim * @author Max Gensthaler */ public class PropertySupport implements Support { private final static Logger LOG = LoggerFactory.getLogger(PropertySupport.class); private static final String DEFAULT_NAME = "#default"; private static final Map<Class, List<PropertyDeclaration>> DECLARATION_CACHE = new HashMap<Class, List<PropertyDeclaration>>(); public static PropertySupport get(PresentationModel model) { Supportable s = (Supportable)model; PropertySupport support = s.getSupportMap().get(PropertySupport.class); if (support == null) { support = new PropertySupport(model); s.getSupportMap().put(PropertySupport.class, support); } return support; } private PropertyChangeListener modelListener; private Map<String, PropertyChangeListener> pclMap = new HashMap<String, PropertyChangeListener>(); private final PresentationModel presentationModel; private final Properties properties = new Properties(new PropertiesListener() { /** * This method is called whenever a property is added or removed. */ public void changed(String propertyName, PresentationModel oldValue, PresentationModel newValue) { if (oldValue != newValue) { onRemove(oldValue, propertyName); onAdd(newValue, propertyName); onPropertyChange(propertyName); presentationModel.getPropertyChangeSupport().firePropertyChange(propertyName, oldValue, newValue); } } private void onRemove(PresentationModel pModel, String name) { if (pModel == null) { return; } else { PropertyChangeListener pcListener = pclMap.remove(name); pModel.removePropertyChangeListener(pcListener); if (pModel instanceof ContextOwner && presentationModel instanceof ContextOwner) { ((ContextOwner)pModel).getContext().removeParent(((ContextOwner)presentationModel).getContext()); } } } private void onAdd(PresentationModel pModel, String name) { if (pModel == null) { return; } else { PropertyChangeListener pcListener = new MyPropertyChangeListener(name); pclMap.put(name, pcListener); pModel.addPropertyChangeListener(pcListener); if (pModel instanceof ContextOwner && presentationModel instanceof ContextOwner) { ((ContextOwner)pModel).getContext().addParent(((ContextOwner)presentationModel).getContext()); } } } }); private void onPropertyChange(String propertyName) { // revalidate other properties List<IOperationPM> ops = new LinkedList<IOperationPM>(); for (String name : properties.names()) { if (name.equals(propertyName)) { // skip; since we don't want to revalidate the property that has // been changed continue; } PresentationModel pModel = properties.get(name); if (pModel instanceof IOperationPM) { ops.add((IOperationPM)pModel); // remember it for later revalidation } else if (pModel != null) { pModel.revalidate(); } } // now revalidate the operations for (IOperationPM op : ops) { op.revalidate(); } // revalidate this.presentationModel.revalidate(); } public PropertySupport(PresentationModel pModel) { if (pModel == null) { throw new IllegalArgumentException("pModel == null"); } this.presentationModel = pModel; } public void refresh() { // TODO (mk) refresh should only update known properties... setup(presentationModel.getClass()); } /** * Setup this <code>PropertySupport</code> by processing all * {@link Property} annotations found in the supported * {@link PresentationModel}. */ public void setup() { setup(presentationModel.getClass()); } /** * Setup this <code>PropertySupport</code> by processing only those * {@link Property} annotations that are found in the given {@link Class} * and all superclasses of the current {@link PresentationModel}. * * @param cls */ public void setup(Class cls) { if (cls == null) { throw new IllegalArgumentException("cls==null"); } putProperties(cls); addPropertyChangeListenerToPM(); } /** * This method adds the {@link #modelListener} to the current * {@link PresentationModel}. */ private void addPropertyChangeListenerToPM() { if (modelListener == null) { modelListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { // This method has been called when any of the models properties has changed, // including 'conventional' bean properties. // Since some properties validation rules could be dependent on these properties, we // have to revalidate them. revalidateProperties(); } }; this.presentationModel.addPropertyChangeListener(modelListener); } } void putProperties(Class cls) { Properties newProps = loadProperties(cls); // Set<String> oldNames = new HashSet<String>(this.properties.names()); for (String name : newProps.names()) { // oldNames.remove(name); PresentationModel newValue = newProps.get(name); Class<? extends PresentationModel> type = newProps.getType(name); this.properties.put(name, newValue, type); } // for (String oldName : oldNames) { // this.properties.remove(oldName); // } } public Iterable<PresentationModel> filterProperties(final PresentationModelFilter filter) { return new FilteredModels(this.properties.models(), filter); } public Collection<PresentationModel> getProperties(PresentationModelFilter filter) { List<PresentationModel> result = new LinkedList<PresentationModel>(); for (PresentationModel pModel : this.properties.models()) { if (filter == null || filter.accept(pModel)) { result.add(pModel); } } return result; } public Collection<String> getPropertyNames() { return properties.names(); } public Class<? extends PresentationModel> getPropertyType(String name) { return properties.getType(name); } public String getName(PresentationModel child) { return properties.getName(child); } public void revalidateProperties() { for (PresentationModel pModel : this.properties.models(true)) { pModel.revalidate(); } } public Collection<PresentationModel> getProperties() { return this.properties.models(true); } public PresentationModel getProperty(String name) { return properties.get(name); } public PresentationModel getProperty(Path path) { PathEvaluation eval = new PathEvaluation(this.presentationModel, path); PathEvaluation.Entry result = eval.getResult(); return result.getValue(); } public <M extends PresentationModel> M putProperty(String name, M value, Class<M> type) { this.properties.put(name, value, type); return value; } public PresentationModel removeProperty(String name) { return this.properties.remove(name); } public void accept(PresentationModelFilter filter, PresentationModelVisitor visitor) { if (visitor == null) { throw new IllegalArgumentException("visitor==null"); } for (PresentationModel child : this.getProperties()) { if (filter == null || filter.accept(child)) { visitor.visit(child); } PropertySupport.get(child).accept(filter, visitor); } } public static abstract class PropertyDeclaration { private final Member member; private final String name; public PropertyDeclaration(String name, Member member) { super(); if (name == null) { throw new IllegalArgumentException("name==null"); } this.name = name; if (member == null) { throw new IllegalArgumentException("member==null"); } this.member = member; } public String getName() { return name; } public Class<? extends PresentationModel> getType() { if (member instanceof Field) { return (Class<? extends PresentationModel>)((Field)member).getType(); } else if (member instanceof Method) { return (Class<? extends PresentationModel>)((Method)member).getReturnType(); } else { throw new Error("Unknown member type: " + member.getClass().getName()); } } public Member getMember() { return member; } public boolean isAbstract() { if (member instanceof Method) { Method m = (Method)member; return Modifier.isAbstract(m.getModifiers()); } else { return false; } } } public static class FieldDecl extends PropertyDeclaration { private final Field field; public FieldDecl(String name, Field field) { super(name, field); this.field = field; } Field getField() { return field; } @Override public String toString() { return "Name: " + super.getName() + "\tField:" + field.toString(); } } public static class MethodDecl extends PropertyDeclaration { private final Method method; public MethodDecl(String name, Method method) { super(name, method); this.method = method; } Method getMethod() { return method; } @Override public String toString() { return "Name: " + super.getName() + "\tMethod: " + method.toString(); } } private static void log(Class cls, List<PropertyDeclaration> decls) { if (LOG.isDebugEnabled()) { StringBuilder builder = new StringBuilder(); builder.append("Properties of class ").append(cls.getName()).append(":\n"); for (PropertyDeclaration decl : decls) { builder.append(decl.getName()).append(":").append(decl.getMember()).append("\n"); } LOG.debug(builder.toString()); } } /** * Returns a list of all non-conflicting, explicit and inplicit property * declarations that can be found in the given class (and its superclasses * and interfaces). * * @param cls * @return a list of all non-conflicting, explicit and inplicit property * declarations of the given class */ public static List<PropertyDeclaration> getPropertyDeclarations(Class cls) { // Find all members that can be interpreted as Property declarations List<PropertyDeclaration> decls = findAllPropertyDeclarations(cls); // Resolve any conflicts in these declarations List<PropertyDeclaration> result = resolveConflicts(decls); log(cls, result); return result; } private static List<PropertyDeclaration> resolveConflicts(List<PropertyDeclaration> decls) { List<PropertyDeclaration> result = new ArrayList<PropertyDeclaration>(decls.size()); // Check for conflicting property declarations based on fields // Only the maximum of one field property declaration for the same key is allowd Map<String, PropertyDeclaration> fieldMap = new HashMap<String, PropertyDeclaration>(); for (PropertyDeclaration currentDecl : new ArrayList<PropertyDeclaration>(decls)) { if (!(currentDecl.getMember() instanceof Field)) { continue; } String key = currentDecl.getName(); PropertyDeclaration oldDecl = fieldMap.get(key); if (oldDecl == null) { // No conflict fieldMap.put(key, currentDecl); } else { // Conflict: property is already declared based on a field member // -> We can't solve this conflict throw new IllegalStateException("Illegal property declaration:\nmember " + currentDecl.getMember() + " shadows " + oldDecl.getMember() + "!"); } } // Check for conflicting property declarations based on fields and methods Map<String, PropertyDeclaration> map = new HashMap<String, PropertyDeclaration>(); for (PropertyDeclaration currentDecl : new ArrayList<PropertyDeclaration>(decls)) { String key = currentDecl.getName(); PropertyDeclaration oldDecl = map.get(key); if (oldDecl == null) { // No conflict map.put(key, currentDecl); result.add(currentDecl); } else { // Conflict: property is already declared // Can we solve it? if (currentDecl.getMember() instanceof Method) { // Method wins // -> replace old declaration map.put(key, currentDecl); result.remove(oldDecl); result.add(currentDecl); result.add(currentDecl); } else if (oldDecl.getMember() instanceof Method) { // Method wins // -> ignore current declaration // Nothing to do } else { // Both declarations are based on field members // -> We can't solve this conflict throw new IllegalStateException("Illegal property declaration:\nmember " + currentDecl.getMember() + " shadows " + oldDecl.getMember() + "!"); } } } return result; } private static List<PropertyDeclaration> findAllPropertyDeclarations(Class cls) { List<PropertyDeclaration> result = DECLARATION_CACHE.get(cls); if (result == null) { result = new ArrayList<PropertyDeclaration>(); findAllPropertyDeclarations(cls, result); DECLARATION_CACHE.put(cls, result); } return result; } private static void findAllPropertyDeclarations(Class currentClass, List<PropertyDeclaration> result) { if (!PresentationModel.class.isAssignableFrom(currentClass)) { // Skip classes that are not of type PresentationModel return; } // Process superclass and interfaces Class superCls = currentClass.getSuperclass(); if (superCls != null) { findAllPropertyDeclarations(superCls, result); } Class[] interfaces = currentClass.getInterfaces(); for (Class i : interfaces) { findAllPropertyDeclarations(i, result); } //// Now process the current class // Check for Convention-over-Configuration boolean hasPropertyAnnotations = hasPropertyAnnotations(currentClass); if (hasPropertyAnnotations) { // The current class contains members that are annotated with @Property // -> Since we implement Convention-over-Configuration we only process these members // Search for methods that are annotated with @Property Method[] allDeclMethods = currentClass.getDeclaredMethods(); Method[] annoDeclMethods = (Method[])filterAnnotatedMembers(allDeclMethods, Property.class).toArray(new Method[0]); List<MethodDecl> methodsWithPropertyDecls = filterPropertyMethods(annoDeclMethods); // Add search result result.addAll(methodsWithPropertyDecls); // Search for fields that are annotated with @Property Field[] allDeclFields = currentClass.getDeclaredFields(); Field[] annoDeclFields = (Field[])filterAnnotatedMembers(allDeclFields, Property.class).toArray(new Field[0]); List<FieldDecl> fieldsWithPropertyDecls = filterPropertyFields(annoDeclFields); // Add search result result.addAll(fieldsWithPropertyDecls); } else { // The current class contains no @Property annotation. // -> We will process all members of type PresentationModel // Search for methods with return type PresentationModel Method[] allDeclMethods = currentClass.getDeclaredMethods(); List<MethodDecl> methodsWithPropertyDecls = filterPropertyMethods(allDeclMethods); // Add search result result.addAll(methodsWithPropertyDecls); // Search for fields of type PresentationModel Field[] allDeclFields = currentClass.getDeclaredFields(); List<FieldDecl> fieldsWithPropertyDecls = filterPropertyFields(allDeclFields); // Add search result result.addAll(fieldsWithPropertyDecls); } } /** * Checks whether the given class has at least one declared member with a * Property annotation. * * @param cls * @return <code>true</code> if the given class has at least one declared * member with a Property annotation. */ private static boolean hasPropertyAnnotations(Class cls) { for (Field field : cls.getDeclaredFields()) { if (hasPropertyAnnotation(field)) { return true; } } for (Method method : cls.getDeclaredMethods()) { if (hasPropertyAnnotation(method)) { return true; } } return false; } /** * Checks whether the given element is annotated with Property. * * @param element * @return <code>true</code> if the given element is annotated with Property */ private static boolean hasPropertyAnnotation(AnnotatedElement element) { return element.isAnnotationPresent(Property.class); } private static <T extends AccessibleObject> List<T> filterAnnotatedMembers(T[] allDecl, Class<Property> annoType) { ArrayList<T> result = new ArrayList<T>(); for (T decl : allDecl) { Annotation anno = decl.getAnnotation(annoType); if (anno != null) { result.add(decl); } } return result; } private static List<MethodDecl> filterPropertyMethods(Method[] potentialPropMethods) { List<MethodDecl> result = new LinkedList<MethodDecl>(); for (Method method : potentialPropMethods) { // The name of this method has to begin with "get" if (false == method.getName().startsWith("get")) { continue; } // The return type of this method has to be assignable from PresentationModel if (false == PresentationModel.class.isAssignableFrom(method.getReturnType())) { continue; } // This method must not have any parameters if (method.getParameterTypes().length != 0) { continue; } Property anno = method.getAnnotation(Property.class); String name; if (anno != null && false == DEFAULT_NAME.equals(anno.value())) { name = anno.value(); } else { name = getFieldname(method); } MethodDecl element = new MethodDecl(name, method); result.add(element); } return result; } private static List<FieldDecl> filterPropertyFields(Field[] potentialPropFields) { List<FieldDecl> result = new LinkedList<FieldDecl>(); for (Field field : potentialPropFields) { // The type of this field has to be assignable from PresentationModel if (false == PresentationModel.class.isAssignableFrom(field.getType())) { continue; } Property anno = field.getAnnotation(Property.class); String name; if (anno != null && false == DEFAULT_NAME.equals(anno.value())) { name = anno.value(); } else { name = field.getName(); } FieldDecl element = new FieldDecl(name, field); result.add(element); } return result; } private Properties loadProperties(Class cls) { try { List<PropertyDeclaration> decls = getPropertyDeclarations(cls); Properties result = new Properties(); for (PropertyDeclaration decl : decls) { Object value = null; if (decl instanceof MethodDecl) { value = ReflectionUtil.invokeMethod(presentationModel, ((MethodDecl)decl).getMethod()); } else if (decl instanceof FieldDecl) { value = ReflectionUtil.getFieldValue(presentationModel, ((FieldDecl)decl).getField()); } else { throw new RuntimeException("Should never be thrown. What should decl else be?"); } if (value == null || value instanceof PresentationModel) { String name = decl.getName(); Class<? extends PresentationModel> type = decl.getType(); if (LOG.isDebugEnabled()) { LOG.debug("Defining property " + decl.getName() + " with: " + value); } result.put(name, (PresentationModel)value, type); } else { throw new IllegalStateException("Return type of member '" + decl.getMember() + "' must implement " + PresentationModel.class.getName()); } } return result; } catch (IllegalAccessException e) { throw new UndeclaredThrowableException(e); } catch (InvocationTargetException e) { throw new UndeclaredThrowableException(e); } } private static String getFieldname(Method getterMethod) { String name = getterMethod.getName(); if (name.startsWith("get")) { return Character.toLowerCase(name.charAt(3)) + name.substring(4); } else if (name.startsWith("is")) { return Character.toLowerCase(name.charAt(2)) + name.substring(3); } else { return name; } } private class MyPropertyChangeListener implements PropertyChangeListener { String propertyName; public MyPropertyChangeListener(String propertyName) { this.propertyName = propertyName; } public void propertyChange(PropertyChangeEvent evt) { onPropertyChange(this.propertyName); // 'forward' this event to listeners on presentationModel // (optimized: do not forward events about the 'modified' property, since that doubles the // number of events) if ("modified".equals(evt.getPropertyName()) == false) { presentationModel.getPropertyChangeSupport().firePropertyChange(this.propertyName, null, null, evt); } } } }