package net.sf.sdedit.ui.components.configuration;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.sdedit.util.DocUtil;
import net.sf.sdedit.util.DocUtil.XMLException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* A <tt>Bean</tt> provides a single instance of a "data object"
* that implements the <tt>T</tt> interface which should only define get-,
* set-, and is-methods like a Java bean. The instance is returned by
* {@linkplain #getDataObject()}. For all manipulations of the data object's
* state (invocations of set-methods) the <tt>Bean</tt> immediately sends
* notifications to all interested <tt>PropertyChangeListener</tt>s. The
* state of the data object can be loaded and stored, using XML documents (see
* {@linkplain #load(Document, String)},
* {@linkplain #store(Document, String, String)}). The values of the data
* object can also be accessed by passing their corresponding properties as
* arguments (see {@linkplain #setValue(PropertyDescriptor, Object)},
* {@linkplain #getValue(String)}).
* <p>
* The values returned by the data object managed by a <tt>Bean</tt> are
* always, provided the bean has been set up/loaded properly, not null (this can
* be enforced by the <tt>permitNullValues</tt> property set to false), and
* legal, i. e. string properties for which there is a set of alternative values
* are always assigned to a legal value.
*
*
* @author Markus Strauch
*
* @param <T>
* the interface type of the data object
*/
public class Bean<T extends DataObject> implements InvocationHandler {
private Set<PropertyChangeListener> listeners;
private SortedMap<String, PropertyDescriptor> properties;
private SortedMap<String, String> order;
// The state of the artificial object accessed by the T proxy
// (see getDataObject())
private HashMap<String, Object> values;
private Class<T> dataClass;
private T dataObject;
private StringSelectionProvider<T> ssp;
private boolean permitNullValues;
private Map<String, Set<String>> stringSets;
private Map<String, String> methodToPropertyNameMap;
private Pattern pattern = Pattern.compile("get|set|is");
/**
* Creates a new bean that provides a single data object. It is not
* permitted to set <tt>null</tt> values for this data object, as long as
* {@linkplain #setPermitNullValues(boolean)} is not called.
*
* @param dataClass
* the interface type of the data object
* @param ssp
* a <tt>StringSelectionProvider</tt> that provides an array of
* strings for methods of the data object which are annotated
* {@linkplain Adjustable#stringSelectionProvided()}
*/
@SuppressWarnings("unchecked")
public Bean(Class<T> dataClass, StringSelectionProvider ssp) {
listeners = new LinkedHashSet<PropertyChangeListener>();
properties = new TreeMap<String, PropertyDescriptor>();
order = new TreeMap<String, String>();
values = new HashMap<String, Object>();
this.ssp = ssp;
this.dataClass = dataClass;
stringSets = new HashMap<String, Set<String>>();
init();
dataObject = (T) Proxy.newProxyInstance(dataClass.getClassLoader(),
new Class[] { dataClass }, this);
permitNullValues = false;
methodToPropertyNameMap = new HashMap<String, String>();
}
private static String norm(String property) {
return Character.toUpperCase(property.charAt(0))
+ property.substring(1);
}
private void init() {
try {
BeanInfo beanInfo = Introspector.getBeanInfo(dataClass);
PropertyDescriptor[] propertyDescriptors = beanInfo
.getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; i++) {
PropertyDescriptor property = propertyDescriptors[i];
if (property.getWriteMethod() != null
&& property.getWriteMethod().isAnnotationPresent(
Adjustable.class)) {
String key = property.getWriteMethod().getAnnotation(
Adjustable.class).key();
if (key.equals("")) {
key = norm(property.getName());
}
order.put(key, norm(property.getName()));
properties.put(norm(property.getName()), property);
if (getValue(property) == null) {
// may not be null when called in the process of deserialization
// (see readObject)
setValue(property,NullValueProvider.getNullValue(property.getPropertyType()));
}
}
}
} catch (RuntimeException e) {
throw e;
} catch (Throwable t) {
t.printStackTrace();
throw new IllegalStateException(
"FATAL: data class introspection was not successful");
}
}
/**
* Returns the synthetic data object implementing the data interface
* belonging to this {@linkplain Bean}.
*
* @return the synthetic data object implementing the data interface
* belonging to this {@linkplain Bean}
*/
public T getDataObject() {
return dataObject;
}
/**
* @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
* java.lang.reflect.Method, java.lang.Object[])
*/
public final Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String name = method.getName();
if ("toString".equals(name) && method.getParameterTypes().length == 0) {
return values.toString();
}
String property = methodToPropertyNameMap.get(name);
if (property == null) {
Matcher matcher = pattern.matcher(name);
property = matcher.replaceFirst("");
methodToPropertyNameMap.put(name, property);
}
// Class cls = null;
// try {
// cls = Class.forName("net.sf.sdedit.config.Configuration");
// } catch (Exception ignored) {
//
// }
// if (dataClass == cls) {
// Integer used = usage.get(property);
// if (used == null) {
// usage.put(property, 1);
// } else {
// usage.put(property, used + 1);
// }
// System.out.println(usage);
// }
if (name.charAt(0) == 's') {
// set-method
setValue(properties.get(property), args[0]);
return null;
}
return getValue(property);
}
/**
* Returns the properties of this Bean that are annotated with an
* {@linkplain Adjustable} annotation.
*
* @return the properties of this Bean that are annotated with an
* {@linkplain Adjustable} annotation
*/
public Collection<PropertyDescriptor> getProperties() {
List<PropertyDescriptor> list = new LinkedList<PropertyDescriptor>();
for (String property : order.values()) {
list.add(properties.get(property));
}
return list;
}
/**
* Returns the <tt>PropertyDescriptor</tt> for the property with the given
* name.
*
* @param name
* the name of a property
* @return the corresponding <tt>PropertyDescriptor</tt> or <tt>null</tt>
* if there is no property with the name
*/
public PropertyDescriptor getProperty(String name) {
return properties.get(norm(name));
}
/**
* Adds a listener that is notified when a property is modified via
* {@linkplain #setValue(PropertyDescriptor, Object)}.
*
* @param listener
* a listener that is notified when a property is modified via
* {@linkplain #setValue(PropertyDescriptor, Object)}
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
listeners.add(listener);
}
/**
* Removes a property change listener.
*
* @param listener
* the listener to be removed
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
listeners.remove(listener);
}
/**
* Changes this bean's properties' values such that they are equal to the
* given bean's properties' values.
*
* @param bean
* another bean
*/
public void takeValuesFrom(Bean<T> bean) {
for (PropertyDescriptor property : getProperties()) {
setValue(property, bean.getValue(property.getName()));
}
}
/**
* Returns a shallow copy of this bean.
*
* @return a shallow copy of this bean
*/
public Bean<T> copy() {
Bean<T> copy = new Bean<T>(dataClass, ssp);
copy.takeValuesFrom(this);
return copy;
}
/**
* Returns the current value of the given property, represented by its name.
*
* @param property
* the name of a property
* @return the current value of the property
*/
public final Object getValue(String property) {
return values.get(norm(property));
}
public final Object getValue (PropertyDescriptor pd) {
return getValue(pd.getName());
}
/**
* Changes this bean's properties' values such that they reflect the values
* found in the given document. It remains unchanged if the document does
* not contain a subtree corresponding to <tt>pathToElement</tt>.
*
* @param document
* a document
* @param pathToElement
* XPath to the subtree where the properties' values are
* described
* @throws XMLException
*/
public void load(Document document, String pathToElement)
throws XMLException {
Element elem = (Element) DocUtil.evalXPathAsNode(document,
pathToElement);
if (elem != null) {
BeanConverter converter = new BeanConverter(this, document);
converter.setValues(elem);
}
}
/**
* Stores all properties' current values in a newly created subtree of the
* given document.
*
* @param document
* the document
* @param pathToParent
* XPath to the parent of the root of the subtree
* @param elementName
* the name of the root of the subtree
* @throws XMLException
*/
public void store(Document document, String pathToParent, String elementName)
throws XMLException {
Element parent = (Element) DocUtil.evalXPathAsNode(document,
pathToParent);
BeanConverter converter = new BeanConverter(this, document);
Element elem = converter.createElement(elementName);
parent.appendChild(elem);
}
/**
* Sets a new value for a property and informs all
* <tt>PropertyChangeListener</tt>s about that. If the
* <tt>admitNullValues</tt> property is false, a new value of
* <tt>null</tt> will be ignored and not set. Furthermore, it is not
* permitted to set a string value for a property that has a set of
* alternative values, if none of these matches. The illegal value will be
* silently ignored.
*
* @param property
* the descriptor of the property
* @param value
* the new value of the property
*/
public final void setValue(PropertyDescriptor property, Object value) {
if (value == null && !permitNullValues) {
return;
}
if (property.getPropertyType() == String.class) {
Set<String> choices = getStringsForProperty(property);
if (!choices.isEmpty() && !choices.contains(value)) {
return;
}
}
String propertyName = norm(property.getName());
Object oldValue = values.get(propertyName);
values.put(propertyName, value);
firePropertyChanged(property, value, oldValue);
}
/**
* Sends a notification about the change of a property provided both values
* are not equal (with respect to the result of <tt>equals(Object)</tt>.
*
* @param property
* the descriptor of the property whose value has changed
* @param newValue
* the new value of the property
* @param oldValue
* the old value of the property
*/
private void firePropertyChanged(PropertyDescriptor property,
Object newValue, Object oldValue) {
if (newValue == null && oldValue == null) {
return;
}
if (newValue == null || oldValue == null || !newValue.equals(oldValue)) {
PropertyChangeEvent event = new PropertyChangeEvent(this, property
.getName(), oldValue, newValue);
for (PropertyChangeListener listener : listeners) {
listener.propertyChange(event);
}
}
}
/**
* Returns true if and only if <tt>o</tt> is a reference to a bean with
* the same properties that have the same values as this bean's properties.
*
* @return true if and only if <tt>o</tt> is a reference to a bean with
* the same properties that have the same values as this bean's
* properties
*/
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object o) {
Bean<? extends DataObject> bean = (Bean<? extends DataObject>) o;
for (PropertyDescriptor property : getProperties()) {
Object myVal = getValue(property.getName());
Object yourVal = bean.getValue(property.getName());
if (myVal == null && yourVal == null) {
// check next property if both are null
continue;
}
if (myVal == null || yourVal == null) {
// if one is null, the other is not, so return false
return false;
}
if (!myVal.equals(yourVal)) {
return false;
}
}
return true;
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
StringBuffer code = new StringBuffer();
for (PropertyDescriptor property : getProperties()) {
Object val = getValue(property.getName());
code.append(val);
}
return code.hashCode();
}
/**
* Returns a set of strings representing all alternative values for the
* given String property. If there is no alternative, i. e. all values are
* possible, the set is empty.
*
* @param property
* a String property
* @return an array of strings representing all alternative values for the
* given String property
*/
public Set<String> getStringsForProperty(PropertyDescriptor property) {
String propName = norm(property.getName());
Set<String> strings = stringSets.get(propName);
if (strings == null) {
strings = new LinkedHashSet<String>();
Adjustable adj = property.getWriteMethod().getAnnotation(
Adjustable.class);
String[] choices = adj.choices();
if (choices.length == 0 && adj.stringSelectionProvided()) {
choices = ssp.getStringSelection(property.getName());
}
for (String choice : choices) {
strings.add(choice);
}
stringSets.put(norm(property.getName()), strings);
}
return strings;
}
public String toString() {
StringBuffer buffer = new StringBuffer();
for (PropertyDescriptor property : getProperties()) {
buffer.append(property.getName() + "=");
buffer.append(getValue(property.getName()));
buffer.append("\n");
}
return buffer.toString();
}
/**
* Returns a flag denoting if <tt>null</tt> values can be used as
* parameters of the data object's set methods.
*
* @return a flag denoting if <tt>null</tt> values can be used as
* parameters of the data object's set methods
*/
public boolean isPermitNullValues() {
return permitNullValues;
}
/**
* Sets a flag denoting if <tt>null</tt> values can be used as parameters
* of the data object's set methods.
*
* @param permitNullValues
* a flag denoting if <tt>null</tt> values can be used as
* parameters of the data object's set methods
*/
public void setPermitNullValues(boolean permitNullValues) {
this.permitNullValues = permitNullValues;
}
}