/*
* 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 java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
/**
* A property is composed of a {@link PropertyDescriptor} (static type description) and
* an {@link PropertyAccessor} (dynamic value assignment).
* The {@link Property} interface is a realisation of the <i>Value Object</i> design pattern.
* Most of the time, properties are used as part of a {@link PropertyContainer}.
*
* @author Norman Fomferra
* @since 0.6
*/
public class Property {
static final HashMap<Class<?>, Object> PRIMITIVE_ZERO_VALUES;
static {
PRIMITIVE_ZERO_VALUES = new HashMap<>(17);
PRIMITIVE_ZERO_VALUES.put(Boolean.TYPE, false);
PRIMITIVE_ZERO_VALUES.put(Character.TYPE, (char) 0);
PRIMITIVE_ZERO_VALUES.put(Byte.TYPE, (byte) 0);
PRIMITIVE_ZERO_VALUES.put(Short.TYPE, (short) 0);
PRIMITIVE_ZERO_VALUES.put(Integer.TYPE, 0);
PRIMITIVE_ZERO_VALUES.put(Long.TYPE, (long) 0);
PRIMITIVE_ZERO_VALUES.put(Float.TYPE, (float) 0);
PRIMITIVE_ZERO_VALUES.put(Double.TYPE, (double) 0);
}
private final PropertyDescriptor descriptor;
private final PropertyAccessor accessor;
private PropertyContainer container;
public Property(PropertyDescriptor descriptor, PropertyAccessor accessor) {
this.descriptor = descriptor;
this.accessor = accessor;
}
public static Property create(String name, Class<?> type) {
return createImpl(createDescriptor(name, type), new DefaultPropertyAccessor(), null);
}
public static Property create(String name, Object value) {
return createImpl(createDescriptor(name, value.getClass()), new DefaultPropertyAccessor(), value);
}
public static <T> Property create(String name, Class<? extends T> type, T defaultValue, boolean notNull) {
final PropertyDescriptor descriptor = createDescriptor(name, type);
if (notNull) {
descriptor.setDefaultValue(defaultValue);
descriptor.setNotNull(true);
}
return new Property(descriptor, new DefaultPropertyAccessor(defaultValue));
}
public static Property createForField(Object object, String name) {
final Field field = getField(object, name);
PropertyDescriptor descriptor = createDescriptor(name, field.getType());
boolean isDeprecated = field.getAnnotation(Deprecated.class) != null;
descriptor.setDeprecated(isDeprecated);
return createImpl(descriptor, new ClassFieldAccessor(object, field));
}
public static Property createForField(Object object, String name, Object value) {
final Field field = getField(object, name);
PropertyDescriptor descriptor = createDescriptor(name, field.getType());
boolean isDeprecated = field.getAnnotation(Deprecated.class) != null;
descriptor.setDeprecated(isDeprecated);
return createImpl(descriptor, new ClassFieldAccessor(object, field), value);
}
public static Property createForMapEntry(Map<String, Object> map, String name, Class<?> type) {
return createImpl(createDescriptor(name, type), new MapEntryAccessor(map, name), null);
}
public static Property createForMapEntry(Map<String, Object> map, String name, Class<?> type, Object value) {
return createImpl(createDescriptor(name, type), new MapEntryAccessor(map, name), value);
}
public PropertyDescriptor getDescriptor() {
return descriptor;
}
public PropertyContainer getContainer() {
return container;
}
public void setContainer(PropertyContainer container) {
this.container = container;
}
public Validator getValidator() {
return descriptor.getEffectiveValidator();
}
public String getValueAsText() {
final Converter converter = descriptor.getConverter(true);
return converter.format(getValue());
}
public void setValueFromText(String text) throws ValidationException {
final Converter converter = descriptor.getConverter(true);
final Object value;
try {
value = converter.parse(text);
} catch (ConversionException e) {
throw new ValidationException(MessageFormat.format("Value for ''{0}'' is invalid.\n''{1}''",
getDescriptor().getDisplayName(),
e.getMessage()), e );
}
setValue(value);
}
public String getName() {
return getDescriptor().getName();
}
public Class<?> getType() {
return getDescriptor().getType();
}
public <T> T getValue() {
final Object value = accessor.getValue();
if (value == null && descriptor.getType().isPrimitive()) {
return (T) PRIMITIVE_ZERO_VALUES.get(descriptor.getType());
}
return (T) value;
}
public void setValue(Object value) throws ValidationException {
Object oldValue = getValue();
if (equalObjects(oldValue, value)) {
return;
}
// todo - test cast castToPropertyType() - needed for Python API, nf 25.06.2013
// value = castToPropertyType(value);
validate(value);
accessor.setValue(value);
if (container != null) {
container.getPropertyChangeSupport().firePropertyChange(descriptor.getName(), oldValue, value);
}
}
// todo - test cast castToPropertyType() - needed for Python API, nf 25.06.2013
/*
private Object castToPropertyType(Object value) {
if (value == null) {
return value;
} else if (getType().isAssignableFrom(value.getClass())) {
return value;
} else if (getType().isArray()) {
if (value.getClass().isArray()) {
return castSourceArrayToTargetArray(value);
} else if (value instanceof List) {
final List list = (List) value;
return castSourceArrayToTargetArray(list.toArray(new Object[list.size()]));
}
} else if (Object.class.isAssignableFrom(getType())) {
if (value instanceof Map) {
PropertySet sourcePS = PropertyContainer.createMapBacked((Map) value, getType());
PropertySet targetPS = PropertyContainer.createObjectBacked(getType().newInstance());
copyPropertySets(sourcePS, targetPS);
} else if (Map.class.isAssignableFrom(getType())) {
PropertySet sourcePS = PropertyContainer.createObjectBacked(value);
PropertySet targetPS = PropertyContainer.createMapBacked(new HashMap());
copyPropertySets(sourcePS, targetPS);
}
}
// No cast possible, validate() will check for us
return value;
}
private Object castSourceArrayToTargetArray(Object sourceArray) throws ValidationException {
Class<?> targetCompType = getType().getComponentType();
int length = Array.getLength(sourceArray);
Object targetArray = Array.newInstance(targetCompType, length);
for (int i = 0; i < length; i++) {
Object sourceElement = Array.get(sourceArray, i);
Property elementProperty = Property.create(String.format("%s[%d]", getName(), i), targetCompType);
// forces recursively calling castToPropertyType() on array elements
elementProperty.setValue(sourceElement);
Array.set(sourceArray, i, elementProperty.getValue());
}
return targetArray;
}
private void copyPropertySets(PropertySet sourcePS, PropertySet targetPS) {
final Property[] sourceProperties = sourcePS.getProperties();
for (Property sourceProperty : sourceProperties) {
if (targetPS.isPropertyDefined(sourceProperty.getName())) {
// forces recursively calling castToPropertyType() on set members
targetPS.setValue(sourceProperty.getName(), sourceProperty.getValue());
}
}
}
*/
private boolean equalObjects(Object oldValue, Object newValue) {
if (oldValue == newValue) {
return true;
}
if (oldValue == null) {
return false;
}
if (oldValue instanceof Number && newValue instanceof Number) {
final Number number = (Number) newValue;
final Object epsValue = descriptor.getAttribute("eps");
if (epsValue instanceof Number) {
if ((oldValue instanceof Float)) {
final float v1 = (Float) oldValue;
final float v2 = number.floatValue();
final float deltaSig = v1 - v2;
final float deltaAbs = Math.abs(deltaSig);
final float eps = ((Number) epsValue).floatValue();
return deltaAbs < eps;
} else if ((oldValue instanceof Double)) {
final double v1 = (Double) oldValue;
final double v2 = number.floatValue();
final double deltaSig = v1 - v2;
final double deltaAbs = Math.abs(deltaSig);
final double eps = ((Number) epsValue).floatValue();
return deltaAbs < eps;
}
}
}
return oldValue.equals(newValue);
}
public void validate(Object value) throws ValidationException {
synchronized (this) {
final Validator validator = getValidator();
if (validator != null) {
validator.validateValue(this, value);
}
}
}
public void addPropertyChangeListener(PropertyChangeListener l) {
if (container == null) {
throw new IllegalStateException("container == null");
}
container.getPropertyChangeSupport().addPropertyChangeListener(descriptor.getName(), l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
if (container == null) {
throw new IllegalStateException("container == null");
}
container.getPropertyChangeSupport().removePropertyChangeListener(descriptor.getName(), l);
}
@Override
public String toString() {
return getClass().getName() + "[name=" + getName() + ",value=" + getValueAsText() + "]";
}
private static PropertyDescriptor createDescriptor(String name, Class<?> type) {
return new PropertyDescriptor(name, type);
}
private static Property createImpl(PropertyDescriptor descriptor, PropertyAccessor accessor) {
Property vm = new Property(descriptor, accessor);
vm.getDescriptor().setConverter(ConverterRegistry.getInstance().getConverter(descriptor.getType()));
return vm;
}
private static Property createImpl(PropertyDescriptor descriptor, PropertyAccessor accessor, Object value) {
Property vm = createImpl(descriptor, accessor);
if (value == null && descriptor.getType().isPrimitive()) {
value = PRIMITIVE_ZERO_VALUES.get(descriptor.getType());
}
try {
vm.setValue(value);
} catch (ValidationException e) {
throw new IllegalStateException(e);
}
return vm;
}
private static Field getField(Object object, String name) {
Field field;
try {
field = object.getClass().getDeclaredField(name);
} catch (NoSuchFieldException e) {
throw new IllegalStateException(e);
}
return field;
}
}