/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package com.bc.ceres.binding; import com.bc.ceres.binding.accessors.ClassFieldAccessor; import com.bc.ceres.binding.accessors.DefaultPropertyAccessor; import com.bc.ceres.binding.accessors.MapEntryAccessor; import com.bc.ceres.core.Assert; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * A convenience implementation of the {@link PropertySet} interface. * {@link PropertyContainer} is basically an implementation of the <i>Property List</i> design pattern. * * @author Norman Fomferra * @since 0.6 */ public class PropertyContainer implements PropertySet { private final Map<String, Property> propertyMap; private final List<Property> propertyList; private final PropertyChangeSupport propertyChangeSupport; /** * Constructs a new, empty property container. */ public PropertyContainer() { propertyMap = new HashMap<>(10); propertyList = new ArrayList<>(10); propertyChangeSupport = new PropertyChangeSupport(this); } /** * Creates a property container for the given object. * The factory method will not modify the object, thus not setting any default values. * * @param object the backing object * @return The property container. */ public static PropertyContainer createObjectBacked(Object object) { return createObjectBacked(object, new DefaultPropertyDescriptorFactory()); } /** * Creates a property container for the given object. * The factory method will not modify the object, thus not setting any default values. * * @param object the backing object * @param descriptorFactory a factory used to create {@link PropertyDescriptor}s of the fields of the object's type * @return The property container. */ public static PropertyContainer createObjectBacked(Object object, PropertyDescriptorFactory descriptorFactory) { return createForFields(object.getClass(), descriptorFactory, new ObjectBackedPropertyAccessorFactory(object), false); } public static PropertyContainer createObjectBacked(Object object, PropertySetDescriptor propertySetDescriptor) { Map<String, Field> fields = getPropertyFields(object.getClass()); PropertyContainer propertySet = new PropertyContainer(); for (String propertyName : propertySetDescriptor.getPropertyNames()) { PropertyDescriptor propertyDescriptor = propertySetDescriptor.getPropertyDescriptor(propertyName); Field field = fields.get(propertyDescriptor.getName()); if (field != null) { propertyDescriptor.initDefaults(); propertySet.addProperty(new Property(propertyDescriptor, new ClassFieldAccessor(object, field))); } } return propertySet; } /** * Creates a property container for a map backing the values. * The properties are derived from the current map entries. * * @param map the map which backs the values * @return The property container. */ public static PropertyContainer createMapBacked(Map<String, Object> map) { PropertyContainer propertyContainer = new PropertyContainer(); for (Entry<String, Object> entry : map.entrySet()) { String name = entry.getKey(); Object value = entry.getValue(); final PropertyDescriptor propertyDescriptor = new PropertyDescriptor(name, value.getClass()); propertyDescriptor.initDefaults(); propertyContainer.addProperty(new Property(propertyDescriptor, new MapEntryAccessor(map, name))); } return propertyContainer; } /** * Creates a property container for a map backing the values. * The factory method will not modify the given map, thus not setting any default values. * * @param map the map which backs the values * @param propertySetDescriptor A descriptor the property set to be created. * @return The property container. */ public static PropertyContainer createMapBacked(Map<String, Object> map, PropertySetDescriptor propertySetDescriptor) { PropertyContainer propertySet = new PropertyContainer(); for (String propertyName : propertySetDescriptor.getPropertyNames()) { PropertyDescriptor propertyDescriptor = propertySetDescriptor.getPropertyDescriptor(propertyName); propertyDescriptor.initDefaults(); propertySet.addProperty(new Property(propertyDescriptor, new MapEntryAccessor(map, propertyName))); } return propertySet; } /** * Creates a property container for the given template type and map backing the values. * The factory method will not modify the given map, thus not setting any default values. * * @param map the map which backs the values * @param templateType the template type * @return The property container. */ public static PropertyContainer createMapBacked(Map<String, Object> map, Class<?> templateType) { return createMapBacked(map, templateType, new DefaultPropertyDescriptorFactory()); } /** * Creates a property container for the given template type and map backing the values. * The factory method will not modify the given map, thus not setting any default values. * * @param map the map which backs the values * @param templateType the template type * @param descriptorFactory a factory used to create {@link PropertyDescriptor}s of the fields of the template type * @return The property container. */ public static PropertyContainer createMapBacked(Map<String, Object> map, Class<?> templateType, PropertyDescriptorFactory descriptorFactory) { return createForFields(templateType, descriptorFactory, new MapBackedPropertyAccessorFactory(map), false); } /** * Creates a property container for the given template type. * All properties will have their values set to default values (if specified). * * @param templateType the template type * @return The property container. */ public static PropertyContainer createValueBacked(Class<?> templateType) { return createValueBacked(templateType, new DefaultPropertyDescriptorFactory()); } /** * Creates a property container for the given template type. * All properties will have their values set to default values (if specified). * * @param templateType the template class used to derive the descriptors from * @param descriptorFactory a factory used to create {@link PropertyDescriptor}s of the fields of the template type * @return The property container. */ public static PropertyContainer createValueBacked(Class<?> templateType, PropertyDescriptorFactory descriptorFactory) { return createForFields(templateType, descriptorFactory, new ValueBackedPropertyAccessorFactory(), true); } /** * Creates a property container for the given template type. Properties are generated * using the {@code descriptorFactory} and the {@code accessorFactory} which * are called for each non-static and non-transient class field. * * @param type The type that provides the fields. * @param descriptorFactory The property descriptor factory. * @param accessorFactory The property accessor factory. * @param initValues If {@code true}, properties are initialised by their default values, if specified. * @return The property container. */ public static PropertyContainer createForFields(Class<?> type, PropertyDescriptorFactory descriptorFactory, PropertyAccessorFactory accessorFactory, boolean initValues) { PropertyContainer container = new PropertyContainer(); collectProperties(type, descriptorFactory, accessorFactory, container); if (initValues) { container.setDefaultValues(); } return container; } static Map<String, Field> getPropertyFields(Class<?> type) { return ClassScanner.getFields(type, new ClassScanner.FieldFilter() { @Override public boolean accept(Field field) { int modifiers = field.getModifiers(); return !(Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers)); } }); } @Override public Property[] getProperties() { return propertyList.toArray(new Property[propertyList.size()]); } @Override public boolean isPropertyDefined(String name) { return propertyMap.containsKey(name); } @Override public Property getProperty(String name) { Assert.notNull(name, "name"); return propertyMap.get(name); } @Override public void addProperty(Property property) { if (propertyMap.put(property.getName(), property) != property) { final String alias = property.getDescriptor().getAlias(); if (alias != null && !alias.isEmpty()) { propertyMap.put(alias, property); } propertyList.add(property); property.setContainer(this); } } @Override public void addProperties(Property... properties) { for (Property property : properties) { addProperty(property); } } @Override public void removeProperty(Property property) { if (propertyMap.remove(property.getName()) != null) { final String alias = property.getDescriptor().getAlias(); if (alias != null && !alias.isEmpty()) { propertyMap.remove(alias); } propertyList.remove(property); property.setContainer(null); } } @Override public void removeProperties(Property... properties) { for (Property property : properties) { removeProperty(property); } } @Override public <T> T getValue(String name) { final Property property = getProperty(name); if (property == null) { return null; } //noinspection unchecked return (T) property.getValue(); } @Override public void setValue(String name, Object value) throws IllegalArgumentException { try { getProperty(name).setValue(value); } catch (ValidationException e) { throw new IllegalArgumentException(e.getMessage(), e); } } @Override public PropertyDescriptor getDescriptor(String name) { final Property property = getProperty(name); if (property == null) { return null; } return getProperty(name).getDescriptor(); } @Override public void setDefaultValues() throws IllegalStateException { for (final Property property : getProperties()) { final PropertyDescriptor descriptor = property.getDescriptor(); if (descriptor.getDefaultValue() != null) { try { property.setValue(descriptor.getDefaultValue()); } catch (ValidationException e) { throw new IllegalStateException(e); } } } } @Override public void addPropertyChangeListener(PropertyChangeListener l) { getPropertyChangeSupport().addPropertyChangeListener(l); } @Override public void addPropertyChangeListener(String name, PropertyChangeListener l) { getPropertyChangeSupport().addPropertyChangeListener(name, l); } @Override public void removePropertyChangeListener(PropertyChangeListener l) { getPropertyChangeSupport().removePropertyChangeListener(l); } @Override public void removePropertyChangeListener(String name, PropertyChangeListener l) { getPropertyChangeSupport().removePropertyChangeListener(name, l); } PropertyChangeSupport getPropertyChangeSupport() { return propertyChangeSupport; } private static void collectProperties(Class<?> type, PropertyDescriptorFactory descriptorFactory, PropertyAccessorFactory accessorFactory, PropertySet propertySet) { Map<String, Field> fields = getPropertyFields(type); for (String key : fields.keySet()) { Field field = fields.get(key); PropertyDescriptor descriptor = descriptorFactory.createValueDescriptor(field); if (descriptor != null) { descriptor.initDefaults(); PropertyAccessor accessor = accessorFactory.createValueAccessor(field); if (accessor != null) { propertySet.addProperty(new Property(descriptor, accessor)); } } } } private static class ObjectBackedPropertyAccessorFactory implements PropertyAccessorFactory { private final Object object; private ObjectBackedPropertyAccessorFactory(Object object) { this.object = object; } @Override public PropertyAccessor createValueAccessor(Field field) { return new ClassFieldAccessor(object, field); } } private static class MapBackedPropertyAccessorFactory implements PropertyAccessorFactory { private final Map<String, Object> map; private MapBackedPropertyAccessorFactory(Map<String, Object> map) { this.map = map; } @Override public PropertyAccessor createValueAccessor(Field field) { return new MapEntryAccessor(map, field.getName()); } } private static class ValueBackedPropertyAccessorFactory implements PropertyAccessorFactory { @Override public PropertyAccessor createValueAccessor(Field field) { return new DefaultPropertyAccessor(); } } }