/* Copyright (c) 2001 - 2008 TOPP - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.catalog.impl; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.Info; import org.geoserver.ows.util.ClassProperties; import org.geoserver.ows.util.OwsUtils; /** * Proxies an object storing any modifications to it. * <p> * Each time a setter is called through this invocation handler, the property * is stored and not set on the underlying object being proxied until * {@link #commit()} is called. When a getter is called through this invocation * handler, the local properties are checked for one that has been previously * set, if found it is returned, if not found the getter is forwarded to the * underlying proxy object being called. * </p> * <p> * Any collections handled through this interface are cloned and client code * obtains a copy. The two collections will be synced on a call to {@link #commit()}. * </p> * * @author Justin Deoliveira, The Open Planning Project * * TODO: this class should use BeanUtils for all reflection stuff * */ public class ModificationProxy implements InvocationHandler, Serializable { /** * the proxy object */ Object proxyObject; /** * reflection helper */ transient ClassProperties cp; /** * "dirty" properties */ HashMap<String,Object> properties; /** * The old values of the live collections (we have to clone them because once * the proxy commits the original map will contain the same values as the new one, * breaking getOldValues() */ HashMap<String,Object> oldCollectionValues; public ModificationProxy(Object proxyObject) { this.proxyObject = proxyObject; } private ClassProperties cp(){ if(cp == null){ this.cp = OwsUtils.getClassProperties(proxyObject.getClass()); } return cp; } /** * Intercepts getter and setter methods. */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String property = null; if ( ( method.getName().startsWith( "get") || method.getName().startsWith( "is" ) ) && method.getParameterTypes().length == 0 ) { //intercept getter to check the dirty property set property = method.getName().substring( method.getName().startsWith( "get") ? 3 : 2 ); if ( properties != null && properties().containsKey( property ) ) { //return the previously set object return properties().get( property ); } else { //if collection, create a wrapper if ( Collection.class.isAssignableFrom( method.getReturnType() ) ) { Collection real = (Collection) method.invoke( proxyObject, null ); if(real == null) { // in this case there is nothing we can do return null; } Collection wrap = real.getClass().newInstance(); wrap.addAll( real ); properties().put( property, wrap ); // we also need to store a clone of the initial state as the collection // might be a live one Collection clone = real.getClass().newInstance(); clone.addAll( real ); oldCollectionValues().put(property, clone); return wrap; } else if(Map.class.isAssignableFrom( method.getReturnType() )) { Map real = (Map) method.invoke( proxyObject, null ); if(real == null) { // in this case there is nothing we can do return null; } Map wrap = real.getClass().newInstance(); wrap.putAll( real ); properties().put( property, wrap ); // we also need to store a clone of the initial state as the collection // might be a live one Map clone = real.getClass().newInstance(); clone.putAll( real ); oldCollectionValues().put(property, clone); return wrap; } else { //proceed with the invocation } } } if ( method.getName().startsWith( "set") && args.length == 1) { //intercept setter and put new value in list property = method.getName().substring( 3 ); properties().put( property, args[0] ); return null; } try{ Object result = method.invoke( proxyObject, args ); //intercept result and wrap it in a proxy if it is another Info object if ( result != null && shouldProxyProperty(result.getClass())) { //avoid double proxy Object o = ModificationProxy.unwrap( result ); if ( o == result ) { result = ModificationProxy.create( result, (Class) method.getReturnType() ); //cache the proxy, in case it is modified itself properties().put( property, result ); } else { //result was already proxied, leave as is } } return result; }catch(InvocationTargetException e){ Throwable targetException = e.getTargetException(); throw targetException; } } public Object getProxyObject() { return proxyObject; } public HashMap<String,Object> getProperties() { return properties(); } public void commit() { synchronized (proxyObject) { //commit changes to the proxy object for ( Map.Entry<String,Object> e : properties().entrySet() ) { String p = e.getKey(); Object v = e.getValue(); //use the getter to figure out the type for the setter try { Method g = getter(p); //handle collection case if ( Collection.class.isAssignableFrom( g.getReturnType() ) ) { Collection c = (Collection) g.invoke(proxyObject,null); c.clear(); c.addAll( (Collection) v ); } else if( Map.class.isAssignableFrom( g.getReturnType() )) { Map m = (Map) g.invoke(proxyObject, null); m.clear(); m.putAll( (Map) v); } else { Method s = setter(p,g.getReturnType()); if ( Info.class.isAssignableFrom( g.getReturnType() ) ) { //another info is the changed property, it could be one of two cases // 1) the info object was changed in place: x.getY().setFoo(...) // 2) a new info object was set x.setY(...) Info original = (Info) g.invoke(proxyObject, null); Info modified = (Info) unwrap(v); if ( original == modified ) { //case 1, in this case get the proxy and commit it if ( v instanceof Proxy ) { ModificationProxy h = handler( v ); if ( h != null && h.isDirty() ) { h.commit(); } } } else if ( s != null ){ //case 2, just call the setter with the new object s.invoke( proxyObject, v ); } else { throw new IllegalStateException( "New info object set, but no setter for it."); } } else { //call the setter s.invoke( proxyObject, v ); } } } catch( Exception ex ) { throw new RuntimeException( ex ); } } //reset properties = null; } } /** * Helper method for determining if a property of a proxied object should also * be proxied. */ boolean shouldProxyProperty(Class propertyType) { if (Catalog.class.isAssignableFrom(propertyType)) { //never proxy the catalog return false; } return Info.class.isAssignableFrom(propertyType); } HashMap<String,Object> properties() { if ( properties != null ) { return properties; } synchronized (this) { if ( properties != null ) { return properties; } properties = new HashMap<String,Object>(); } return properties; } HashMap<String,Object> oldCollectionValues() { if ( oldCollectionValues != null ) { return oldCollectionValues; } synchronized (this) { if ( oldCollectionValues != null ) { return oldCollectionValues; } oldCollectionValues = new HashMap<String,Object>(); } return oldCollectionValues; } /** * Flag which indicates whether any properties of the object being proxied * are changed. */ public boolean isDirty() { boolean dirty = false; for ( Iterator i = properties().entrySet().iterator(); i.hasNext() && !dirty; ) { Map.Entry e = (Map.Entry) i.next(); if ( e.getValue() instanceof Proxy ) { ModificationProxy h = handler( e.getValue() ); if ( h != null && !h.isDirty() ) { continue; } } else { try { Object orig = unwrap( getter((String) e.getKey()).invoke(proxyObject, null)); if ( orig == null ) { if(e.getValue() == null) { continue; } } else if(e.getValue() != null && orig.equals(e.getValue())) { continue; } } catch(Exception ex) { throw new RuntimeException(ex); } } dirty = true; } return dirty; } List<String> getDirtyProperties() { List<String> propertyNames = new ArrayList<String>(); for ( String propertyName : properties().keySet() ) { //in the case this property is another proxy, check that it is actually dirty Object value = properties.get( propertyName ); if ( value instanceof Proxy ) { ModificationProxy h = handler( value ); if (h != null && !h.isDirty()) { //proxy reports it is not dirty, only return this property if the underling // value is not the same as the current value of the property on the object Object curr = unwrap( value ); try { Object orig = unwrap( getter( propertyName ).invoke( proxyObject, null)); if ( curr == orig ) { continue; } } catch (Exception e) { throw new RuntimeException( e ); } } } propertyNames.add( propertyName ); } return propertyNames; } /** * Returns the names of any changed properties. */ public List<String> getPropertyNames() { List<String> propertyNames = getDirtyProperties(); for ( int i = 0; i < propertyNames.size(); i++ ) { String name = propertyNames.get( i ); propertyNames.set( i , Character.toLowerCase( name.charAt( 0 ) ) + name.substring(1) ); } return propertyNames; } /** * Returns the old values of any changed properties. */ public List<Object> getOldValues() { List<Object> oldValues = new ArrayList<Object>(); for ( String propertyName : getDirtyProperties() ) { if(oldCollectionValues().containsKey(propertyName)) { oldValues.add(oldCollectionValues.get(propertyName)); } else { try { Method g = getter(propertyName); if ( g == null ) { throw new IllegalArgumentException( "No such property: " + propertyName ); } oldValues.add( g.invoke( proxyObject, null ) ); } catch (Exception e) { throw new RuntimeException( e ); } } } return oldValues; } /** * Returns the new values of any changed properties. */ public List<Object> getNewValues() { ArrayList newValues = new ArrayList(); for ( String propertyName : getDirtyProperties()) { newValues.add( properties().get( propertyName ) ); } return newValues; } /* * Helper method for looking up a getter method. */ Method getter( String propertyName ) { Method g = null; try { g = proxyObject.getClass().getMethod( "get" + propertyName , null ); } catch( NoSuchMethodException e1 ) { //could be boolean try { g = proxyObject.getClass().getMethod( "is" + propertyName , null ); } catch( NoSuchMethodException e2 ) {} } if ( g == null ) { g = cp().getter(propertyName, null); } return g; } /* * Helper method for looking up a getter method. */ Method setter( String propertyName, Class type ) { Method s = null; try { s = proxyObject.getClass().getMethod( "set" + propertyName, type ); } catch( NoSuchMethodException e ) { s = cp().setter(propertyName, type); } return s; } /** * Wraps an object in a proxy. * * @throws RuntimeException If creating the proxy fails. */ public static <T> T create( T proxyObject, Class<T> clazz ) { InvocationHandler h = new ModificationProxy( proxyObject ); // proxy all interfaces implemented by the source object List<Class> proxyInterfaces = (List) Arrays.asList( proxyObject.getClass().getInterfaces() ); // ensure that the specified class is included boolean add = true; for ( Class interfce : proxyObject.getClass().getInterfaces() ) { if ( clazz.isAssignableFrom( interfce) ) { add = false; break; } } if( add ) { // make the list mutable (Arrays.asList is not) and then add the extra interfaces proxyInterfaces = new ArrayList<Class>(proxyInterfaces); proxyInterfaces.add( clazz ); } Class proxyClass = Proxy.getProxyClass( clazz.getClassLoader(), (Class[]) proxyInterfaces.toArray(new Class[proxyInterfaces.size()]) ); T proxy; try { proxy = (T) proxyClass.getConstructor( new Class[] { InvocationHandler.class }).newInstance(new Object[] { h } ); } catch( Exception e ) { throw new RuntimeException( e ); } return proxy; } /** * Wraps a list in a decorator which proxies each item in the list. * */ public static <T> List<T> createList( List<T> proxyList, Class<T> clazz ) { return new list( proxyList, clazz ); } /** * Wraps a proxy instance. * <p> * This method is safe in that if the object passed in is not a proxy it is * simply returned. If the proxy is not an instance of {@link ModificationProxy} * it is also returned untouched. *</p> * */ public static <T> T unwrap( T object ) { if ( object instanceof Proxy ) { ModificationProxy h = handler( object ); if ( h != null ) { return (T) h.getProxyObject(); } } if ( object instanceof ProxyList ) { return (T) ((ProxyList)object).proxyList; } return object; } /** * Returns the ModificationProxy invocation handler for an proxy object. * <p> * This method will return null in the case where the object is not a proxy, or * it is being proxies by another invocation handler. * </p> */ public static ModificationProxy handler( Object object ) { if ( object instanceof Proxy ) { InvocationHandler h = Proxy.getInvocationHandler( object ); if ( h instanceof ModificationProxy ) { return (ModificationProxy) h; } } return null; } static class list<T> extends ProxyList { list( List<T> list, Class<T> clazz ) { super( list, clazz ); } protected <T> T createProxy(T proxyObject, Class<T> proxyInterface) { return ModificationProxy.create( proxyObject, proxyInterface ); } protected <U> U unwrapProxy(U proxy, java.lang.Class<U> proxyInterface) { return ModificationProxy.unwrap( proxy ); }; } }