/*
* Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center
*
* 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 org.fhcrc.cpl.toolbox.datastructure;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.log4j.Logger;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.io.Serializable;
import java.io.IOException;
public class BoundMap extends AbstractMap<String,Object> implements Serializable
{
static final HashMap<Class,HashMap<String,BoundProperty>> _savedPropertyMaps = new HashMap<Class,HashMap<String,BoundProperty>>();
protected Object _bean;
transient protected HashMap<String,Object> _map = new HashMap<String,Object>();
transient protected HashMap<String,BoundProperty> _properties;
transient private Object _keyDebug = null;
static class BoundProperty
{
BoundProperty(Method get, Method set, Class type)
{
_getter = get;
_setter = set;
_type = type;
}
Method _getter;
Method _setter;
Class _type;
}
public BoundMap(Object bean)
{
_bean = bean;
initialize(_bean.getClass());
}
public BoundMap()
{
}
public void setBean(Object bean)
{
_bean = bean;
initialize(_bean.getClass());
}
public Object getBean()
{
return _bean;
}
/** Magic method for deserialization */
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
initialize(_bean.getClass());
}
@SuppressWarnings({"CloneDoesntCallSuperClone"})
@Override
public Object clone() throws CloneNotSupportedException
{
throw new CloneNotSupportedException();
}
@Override
public Object get(Object key)
{
String sKey = (String)key;
try
{
BoundProperty bound = getBoundProperty(sKey);
if (null != bound)
{
if (null == bound._getter)
throw new IllegalArgumentException("Can not get property " + sKey);
return bound._getter.invoke(_bean);
}
}
catch (IllegalAccessException x)
{
throw new RuntimeException(x);
}
catch (InvocationTargetException x)
{
throw new RuntimeException(x);
}
return _map.get(sKey);
}
@Override
public Object put(String key, Object value)
{
try
{
BoundProperty bound = getBoundProperty(key);
if (null != bound)
{
Object previous = null;
if (null == bound._setter)
throw new IllegalArgumentException("Can not set property " + key);
if (null != value && !bound._type.isAssignableFrom(value.getClass()))
value = ConvertUtils.convert(value.toString(), bound._type);
if (null != bound._getter)
previous = bound._getter.invoke(_bean);
if (value != previous)
{
assert key != _keyDebug : "infinite recursion???";
//noinspection ConstantConditions
assert null != (_keyDebug = key);
bound._setter.invoke(_bean, value);
}
return previous;
}
}
catch (IllegalAccessException x)
{
throw new RuntimeException(x);
}
catch (InvocationTargetException x)
{
throw new RuntimeException(x);
}
finally
{
//noinspection ConstantConditions
assert null == (_keyDebug = null);
}
return _map.put(key, value);
}
@Override
public Set<Map.Entry<String,Object>> entrySet()
{
Set<String> keys = keySet();
Set<Map.Entry<String,Object>> entries = new HashSet<Map.Entry<String,Object>>();
for (String key : keys)
{
entries.add(new Entry(key));
}
return Collections.unmodifiableSet(entries);
}
private class Entry implements Map.Entry<String,Object>
{
String key;
Entry(String key)
{
this.key = key;
}
public String getKey()
{
return key;
}
public Object getValue()
{
return get(key);
}
public Object setValue(Object v)
{
return put(key, v);
}
}
private String convertToPropertyName(String name)
{
if (1 == name.length())
return name.toLowerCase();
if (Character.isUpperCase(name.charAt(0)) && !Character.isUpperCase(name.charAt(1)))
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
else
return name;
}
private BoundProperty getBoundProperty(String key)
{
BoundProperty bound = _properties.get(key);
if (null == bound && Character.isUpperCase(key.charAt(0)))
bound = _properties.get(convertToPropertyName(key));
return bound;
}
@Override
public Set<String> keySet()
{
Set<String> keys = new HashSet<String>();
Set<String> mapKeys = _map.keySet();
keys.addAll(mapKeys);
Set<String> propKeys = _properties.keySet();
keys.addAll(propKeys);
return Collections.unmodifiableSet(keys);
}
@Override
public int size()
{
return _map.size() + _properties.size();
}
@Override
public boolean containsKey(Object key)
{
String sKey = (String)key;
if (null != _properties.get(sKey))
return true;
return _map.containsKey(sKey);
}
@Override
public Object remove(Object key)
{
String sKey = (String)key;
if (null != _properties.get(sKey))
throw new UnsupportedOperationException("can't remove property " + key);
return _map.remove(sKey);
}
public Map getExtendedProperties()
{
return _map;
}
private void initialize(Class beanClass)
{
synchronized(_savedPropertyMaps)
{
HashMap<String,BoundProperty> props = _savedPropertyMaps.get(beanClass);
if (props == null)
{
try
{
props = new HashMap<String,BoundProperty>();
BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
if (propertyDescriptors != null)
{
for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
{
if (propertyDescriptor != null)
{
String name = propertyDescriptor.getName();
if ("class".equals(name))
continue;
Method readMethod = propertyDescriptor.getReadMethod();
Method writeMethod = propertyDescriptor.getWriteMethod();
Class aType = propertyDescriptor.getPropertyType();
props.put(name, new BoundProperty(readMethod, writeMethod, aType));
}
}
}
}
catch (IntrospectionException e)
{
Logger.getLogger(this.getClass()).error("error creating BoundMap", e);
throw new RuntimeException(e);
}
_savedPropertyMaps.put(beanClass, props);
}
_properties = props;
}
}
public static class TestBean
{
private int i;
private Integer j;
private String s;
public int getI()
{
return i;
}
public void setI(int i)
{
this.i = i;
}
public Integer getJ()
{
return j;
}
public void setJ(Integer j)
{
this.j = j;
}
public String getS()
{
return s;
}
public void setS(String s)
{
this.s = s;
}
}
public static void main(String[] args)
{
TestBean bean = new TestBean();
Map<String,Object> m = new BoundMap(bean);
System.out.println("i=" + m.get("i"));
System.out.println("j=" + m.get("j"));
System.out.println("s=" + m.get("s"));
bean.i = 1;
bean.j = 2;
bean.s = "fred";
System.out.println("i=" + m.get("i"));
System.out.println("j=" + m.get("j"));
System.out.println("s=" + m.get("s"));
m.put("i", "3");
m.put("j", 4);
m.put("s", "velma");
m.put("t", "shaggy");
System.out.println("i=" + bean.i);
System.out.println("j=" + bean.j);
System.out.println("s=" + bean.s);
System.out.println("t=" + m.get("t"));
}
}