/* Copyright (2007-2012) Schibsted ASA * This file is part of Possom. * * Possom is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Possom 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Possom. If not, see <http://www.gnu.org/licenses/>. * * BeanDataObjectInvocationHandler.java * * Created on 23 January 2007, 21:34 * */ package no.sesat.search.datamodel; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.beans.beancontext.BeanContext; import java.beans.beancontext.BeanContextChild; //import java.beans.beancontext.BeanContextSupport; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.ReentrantReadWriteLock; import no.sesat.search.datamodel.BeanDataModelInvocationHandler.DataModelBeanContextSupport; import no.sesat.search.datamodel.access.AccessAllow; import no.sesat.search.datamodel.access.AccessDisallow; import no.sesat.search.datamodel.access.ControlLevel; import no.sesat.search.datamodel.access.DataModelAccessException; import no.sesat.search.datamodel.generic.DataObject.Property; import no.sesat.search.datamodel.generic.MapDataObject; import no.sesat.search.datamodel.generic.MapDataObjectSupport; import no.sesat.search.datamodel.generic.StringDataObject; import no.sesat.search.datamodel.generic.StringDataObjectSupport; import org.apache.commons.beanutils.MappedPropertyDescriptor; import org.apache.log4j.Logger; /** * * * @version <tt>$Id$</tt> */ class BeanDataObjectInvocationHandler<T> implements InvocationHandler, Serializable { // Constants ----------------------------------------------------- private static final Map<Property[], WeakReference<BeanDataObjectInvocationHandler<?>>> instances = new HashMap<Property[], WeakReference<BeanDataObjectInvocationHandler<?>>>(); private static final ReentrantReadWriteLock instancesLock = new ReentrantReadWriteLock(); private static final Logger LOG = Logger.getLogger(BeanDataObjectInvocationHandler.class); private static final boolean ACCESS_CONTROLLED = !Boolean.getBoolean("sesat.datamodel.accesscontrol.ignore"); // Attributes ---------------------------------------------------- private Class<T> implementOf; private Object support; private boolean immutable; // properties: the only part of this class that can be immutable and reused (see proposal SEARCH-1609) protected List<Property> properties = new CopyOnWriteArrayList<Property>(); protected BeanContext context; // Most DataObjects dont have more than 3 properties. // max currency in any mode is typically ~20, but unlikely for even two threads to update at the same time. private transient Map<Method,InvocationTarget> invocationTargetCache = new ConcurrentHashMap<Method,InvocationTarget>(5, 0.75f, 2); // many DataObjects never use a support object so initialCapacity is zero. // max currency in any mode is typically ~20, but unlikely for even two threads to update at the same time. private transient Map<Method,Method> supportMethodCache = new ConcurrentHashMap<Method,Method>(0, 0.75f, 2); private volatile transient String toString = null; // Static -------------------------------------------------------- @SuppressWarnings("unchecked") static <T> BeanDataObjectInvocationHandler<T> instanceOf(final Class<T> cls, final Property... properties) throws IntrospectionException{ BeanDataObjectInvocationHandler instance; if(isImmutable(cls)){ try{ instancesLock.readLock().lock(); instance = instances.get(properties).get(); }finally{ instancesLock.readLock().unlock(); } if(null == instance){ try{ instancesLock.writeLock().lock(); instance = new BeanDataObjectInvocationHandler<T>(cls, properties); instances.put(properties, new WeakReference<BeanDataObjectInvocationHandler<?>>(instance)); }finally{ instancesLock.writeLock().unlock(); } } }else{ instance = new BeanDataObjectInvocationHandler<T>(cls, properties); } return instance; } static String toString(final List<Property> properties){ final StringBuilder builder = new StringBuilder(64 * properties.size() + 8); builder.append('{'); for(Property property : properties){ builder.append(property.getName() + ':' + property.getValue() + ';'); } builder.append('}'); return builder.toString(); } // Constructors -------------------------------------------------- /** No-arg constructor to support serialization */ protected BeanDataObjectInvocationHandler() { implementOf = null; context = new BeanContextSupport(); support = new Object(); immutable = false; }; @SuppressWarnings("unchecked") private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { invocationTargetCache = new ConcurrentHashMap<Method,InvocationTarget>(5, 0.75f, 2); supportMethodCache = new ConcurrentHashMap<Method,Method>(0, 0.75f, 2); implementOf = (Class<T>) stream.readObject(); context = (BeanContext) stream.readObject(); support = stream.readObject(); immutable = stream.readBoolean(); properties = (List<Property>) stream.readObject(); toString = null; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.writeObject(implementOf); stream.writeObject(context); stream.writeObject(support); stream.writeBoolean(immutable); stream.writeObject(properties); } /** Creates a new instance of ProxyBeanDataObject */ protected BeanDataObjectInvocationHandler( final Class<T> cls, final Property... properties) throws IntrospectionException { this(cls, new BeanContextSupport(), properties); } /** Creates a new instance of ProxyBeanDataObject */ @SuppressWarnings("unchecked") protected BeanDataObjectInvocationHandler( final Class<T> cls, final BeanContext context, final Property... properties) throws IntrospectionException { implementOf = cls; this.context = context; final List<Property> propertiesLeftToAdd = new ArrayList<Property>(Arrays.asList(properties)); if( StringDataObject.class.isAssignableFrom(implementOf) ){ String value = null; boolean found = false; for(Property p : properties){ if("string".equals(p.getName())){ value = (String)p.getValue(); propertiesLeftToAdd.remove(p); found = true; break; } } support = found ? new StringDataObjectSupport(value) : null; }else if( MapDataObject.class.isAssignableFrom(implementOf)){ Map<?,?> map = null; boolean found = false; for(Property p : properties){ if("values".equals(p.getName())){ map = (Map<?,?>)p.getValue(); propertiesLeftToAdd.remove(p); found = true; break; } } support = found ? new MapDataObjectSupport(map) : null; }else{ support = null; } for(Property p : propertiesLeftToAdd){ assert checkPropertyClass(implementOf, p) : "Property '" + p.getName() + "' not of right class"; assert isSerializable(p.getValue()) : "Property value not serializable: " + p.getName() + " " + p.getValue(); addProperty(p); } this.immutable = isImmutable(cls); } private boolean isSerializable(final Object obj) { boolean correct = false; if (obj == null) { return true; } try { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ObjectOutputStream os = new ObjectOutputStream(baos); os.writeObject(obj); correct = true; } catch (NotSerializableException e) { /* Do nothing, return value already set */ } catch (IOException e) { /* Do nothing, return value already set */ } return correct; } private boolean checkPropertyClass(final Class cls, final Property property) { boolean correct = false; if (property.getValue() == null) { return true; } try { PropertyDescriptor[] descriptors = Introspector.getBeanInfo(cls).getPropertyDescriptors(); for (PropertyDescriptor descriptor : descriptors) { if (descriptor.getName().equals(property.getName())) { final Class<?> propertyType = property.getValue().getClass().equals(MapDataObjectSupport.class) ? Map.class : property.getValue().getClass(); correct |= descriptor.getPropertyType() == null || descriptor.getPropertyType().isAssignableFrom(propertyType); break; } } } catch (IntrospectionException e) { /* Do nothing, return value already set */ } return correct; } // Public -------------------------------------------------------- /** {@inherit} **/ public Object invoke(final Object obj, final Method method, final Object[] args) throws Throwable { assureAccessAllowed(method); final boolean setter = method.getName().startsWith("set"); final String propertyName = method.getName().replaceFirst("is|get|set", ""); final InvocationTarget invocationTarget = invocationTargetCache.get(method); if(InvocationTarget.SUPPORT == invocationTarget || null == invocationTarget){ final Method invokeSupportMethod = null != supportMethodCache.get(method) ? supportMethodCache.get(method) : findSupport(propertyName, setter); if(null != invokeSupportMethod){ if(null == invocationTarget){ invocationTargetCache.put(method, InvocationTarget.SUPPORT); supportMethodCache.put(method, invokeSupportMethod); } return invokeSupport(invokeSupportMethod, support, args); } } if(InvocationTarget.PROPERTY == invocationTarget || null == invocationTarget){ final Property invokePropertyResult = invokeProperty(propertyName, setter, args); if(null != invokePropertyResult){ if(null == invocationTarget){ invocationTargetCache.put(method, InvocationTarget.PROPERTY); } return invokePropertyResult.getValue() instanceof MapDataObject && Map.class.isAssignableFrom(method.getReturnType()) ? ((MapDataObject)invokePropertyResult.getValue()).getValues() : invokePropertyResult.getValue(); } } if(InvocationTarget.SELF == invocationTarget || null == invocationTarget){ final Object invokeSelfResult = invokeSelf(method, args); if(null != invokeSelfResult){ if(null == invocationTarget){ invocationTargetCache.put(method, InvocationTarget.SELF); } return invokeSelfResult; } } throw new IllegalArgumentException("Method to invoke is not a getter or setter to any bean property: " + method.getName()); } @Override public String toString(){ if(null == toString){ toString = implementOf.getSimpleName() + " [Proxy (" + getClass().getSimpleName() + ")] w/ " + toString(properties); } return toString; } // Package protected --------------------------------------------- BeanContextChild getBeanContextChild(){ return context; } // Protected ----------------------------------------------------- protected final void assureAccessAllowed(final Method method) throws IllegalAccessException{ // we need the current ControlLevel BeanContext beanContext = context; while(null != beanContext.getBeanContext()){ beanContext = beanContext.getBeanContext(); } if(beanContext instanceof DataModelBeanContextSupport){ final ControlLevel level = ((DataModelBeanContextSupport)beanContext).getControlLevel(); final AccessAllow allow = method.getAnnotation(AccessAllow.class); final AccessDisallow disallow = method.getAnnotation(AccessDisallow.class); if(LOG.isTraceEnabled()){ LOG.trace("level " + level); LOG.trace("method " + method); LOG.trace("allow " + allow); LOG.trace("disallow " + disallow); } boolean allowed = false; boolean disallowed = false; if(null != allow){ for(ControlLevel cl : allow.value()){ allowed |= cl == level; } } if(null != disallow){ for(ControlLevel cl : disallow.value()){ disallowed |= cl == level; } } if(ACCESS_CONTROLLED && ((null != allow && !allowed) || (null != disallow && disallowed))){ throw new DataModelAccessException(method, level); } } } protected Object invokeSupport(final Method m, final Object support, final Object[] args){ Object result = null; try{ // if( !support.getClass().isAssignableFrom(m.getDeclaringClass()) ){ // // try to find method again since m is an override and won't be found as is in support // m = support.getClass().getMethod(m.getName(), m.getParameterTypes()); // } result = m.invoke(support, args); }catch(IllegalAccessException iae){ LOG.info(iae.getMessage(), iae); }catch(IllegalArgumentException iae){ LOG.trace(iae.getMessage()); // }catch(NoSuchMethodException nsme){ // LOG.trace(nsme.getMessage()); }catch(InvocationTargetException ite){ LOG.info(ite.getMessage(), ite); } return result; } @SuppressWarnings("unchecked") protected Property invokeProperty(final String propertyName, final boolean setter, final Object[] args) { Property result = null; for (int i = 0; i < properties.size(); ++i) { final Property p = properties.get(i); if (p.getName().equalsIgnoreCase(propertyName)) { if (setter) { // set the new child dataObject if (p.getValue() instanceof MapDataObject && args.length > 1) { final MapDataObject mpd = (MapDataObject) p.getValue(); // detach the old contextChild removeChild(mpd.getValue((String) args[0])); // update property mpd.setValue((String) args[0], args[1]); // add the new contextChild addChild(args[1]); } else { // detach the old contextChild removeChild(p.getValue()); // update property properties.set(i, new Property(p.getName(), args[0])); toString = null; // add the new contextChild addChild(args[0]); } } result = null != p && null != args && p.getValue() instanceof MapDataObject && args.length > (setter ? 1 : 0) ? new Property((String) args[0], ((MapDataObject) p.getValue()).getValue((String) args[0])) : p; break; } } return result; } protected Object invokeSelf(final Method method, final Object[] args){ // try invoking one of our own methods. (Works for example on methods declared by the Object class). Object result = null; // a quick optimisation is to check if there's any method at all with the same name. final Method[] knownMethods = this.getClass().getMethods(); boolean hasSameNameMethod = false; for(Method m : knownMethods){ if(m.getName().equals(method.getName())){ hasSameNameMethod = true; break; } } if(hasSameNameMethod){ try{ result = method.invoke(this, args); }catch(IllegalAccessException iae){ LOG.info(iae.getMessage(), iae); }catch(IllegalArgumentException iae){ LOG.debug(iae.getMessage()); }catch(InvocationTargetException ite){ LOG.info(ite.getMessage(), ite); } } return result; } /** * obj may be null. */ protected void addChild(final Object obj) { // does nothing. DataObject don't have children. } /** * obj may be null. */ protected void removeChild(final Object obj) { // does nothing. DataObject don't have children. } // Private ------------------------------------------------------- private Method findSupport(final String propertyName, final boolean setter) throws IntrospectionException{ // If there's a support instance, use it first. Method m = null; if( null != support ){ final PropertyDescriptor[] propDescriptors = Introspector.getBeanInfo(support.getClass().getInterfaces()[0]).getPropertyDescriptors(); for( PropertyDescriptor pd : propDescriptors ){ if( propertyName.equalsIgnoreCase(pd.getName()) ){ if(pd instanceof MappedPropertyDescriptor ){ final MappedPropertyDescriptor mpd = (MappedPropertyDescriptor)pd; m = setter ? mpd.getMappedWriteMethod() : mpd.getMappedReadMethod(); }else{ m = setter ? pd.getWriteMethod() : pd.getReadMethod(); } break; } } } return m; } private boolean addProperty(final Property property){ // clone it, so caller cannot alter value later final boolean result = properties.add(new Property(property.getName(), property.getValue())); toString = null; return result; } /** return true if any of the propertyDescriptors have a setter method. * also needs to ensure property's type is immutable too ?! **/ private static boolean isImmutable(final Class<?> cls) throws IntrospectionException{ // during development (see proposal SEARCH-1609 Immutability and weakReference caching within the DataModel) // just return false return false; // final PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(cls).getPropertyDescriptors(); // // boolean result = false; // for(PropertyDescriptor property : propertyDescriptors){ // result |= null == property.getReadMethod(); // } // // return result; } // Inner classes ------------------------------------------------- private enum InvocationTarget{ PROPERTY, SELF, SUPPORT; } }