/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.catalog.impl; import java.io.ObjectStreamException; 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.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogInfo; import org.geoserver.catalog.Info; import org.geoserver.catalog.MetadataMap; import org.geoserver.ows.util.ClassProperties; import org.geoserver.ows.util.OwsUtils; import org.geoserver.platform.GeoServerExtensions; import org.geotools.factory.CommonFactoryFinder; import org.opengis.filter.FilterFactory; /** * 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 WrappingProxy, 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.getParameterCount() == 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 = ModificationProxyCloner.cloneCollection(real, true); 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 = ModificationProxyCloner.cloneCollection(real, false); 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 = ModificationProxyCloner.cloneMap(real, true); 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 = ModificationProxyCloner.cloneMap(real, false); 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 ); // in case this is a live indirection, resolve it. Typically this means // the reference is dangling, and we are going to avoid a wrapper around null if (result instanceof Proxy && Proxy.getInvocationHandler(result) instanceof ResolvingProxy) { ResolvingProxy rp = ProxyUtils.handler(result, ResolvingProxy.class); // try to resolve, and return null if the reference is dangling final Catalog catalog = (Catalog) GeoServerExtensions.bean("catalog"); result = rp.resolve(catalog, result); } //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(); } @SuppressWarnings("rawtypes") 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(); for (Object o : (Collection) v) { c.add(unwrap(o)); } } else if( Map.class.isAssignableFrom( g.getReturnType() )) { Map proxied = (Map) v; Map m = (Map) g.invoke(proxyObject, null); m.clear(); for (Object key : proxied.keySet()) { Object uk = unwrap(key); final Object value = proxied.get(key); Object uv = unwrap(value); m.put(uk, uv); } } 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; } private Object readResolve() throws ObjectStreamException { // replace the main proxy object if(proxyObject instanceof CatalogInfo) { CatalogInfo replacement = replaceCatalogInfo((CatalogInfo) proxyObject); if(replacement != null) { proxyObject = unwrap(replacement); } } // any dirty property value if(properties != null) { for (Entry<String, Object> property : properties.entrySet()) { Object value = property.getValue(); if(value instanceof CatalogInfo) { CatalogInfo replacement = replaceCatalogInfo((CatalogInfo) value); if(replacement != null) { property.setValue(unwrap(replacement)); } } else if(value instanceof Collection) { Collection clone = cloneCollection((Collection) value); property.setValue(clone); } else if(value instanceof MetadataMap) { MetadataMap clone = cloneMetadataMap((MetadataMap) value); property.setValue(clone); } } } // and eventually also contents of old collections, they might also be if(oldCollectionValues != null) { for (Entry<String, Object> oce : oldCollectionValues.entrySet()) { Object value = oce.getValue(); if(value instanceof Collection) { Collection oldCollection = (Collection) value; Collection clone = cloneCollection(oldCollection); oce.setValue(clone); } else if(value instanceof MetadataMap) { MetadataMap clone = cloneMetadataMap((MetadataMap) value); oce.setValue(clone); } } } return this; } private MetadataMap cloneMetadataMap(MetadataMap original) { MetadataMap clone = new MetadataMap(); for (Entry<String, Serializable> entry : original.entrySet()) { String key = entry.getKey(); Serializable value = entry.getValue(); if(value instanceof CatalogInfo) { CatalogInfo replacement = replaceCatalogInfo((CatalogInfo) value); if(replacement != null) { value = replacement; } } clone.put(key, value); } return clone; } private Collection cloneCollection(Collection oldCollection) { Class<? extends Collection> oldCollectionClass = oldCollection.getClass(); try { Collection clone = oldCollectionClass.newInstance(); for (Object o : oldCollection) { if(o instanceof CatalogInfo) { CatalogInfo replacement = replaceCatalogInfo((CatalogInfo) o); if(replacement != null) { clone.add(unwrap(replacement)); } else { clone.add(o); } } else { clone.add(o); } } return clone; } catch(Exception e) { throw new RuntimeException("Unexpected failure while cloning collection of class " + oldCollectionClass, e); } } private CatalogInfo replaceCatalogInfo(CatalogInfo ci) { String id = ci.getId(); Catalog catalog = (Catalog) GeoServerExtensions.bean("catalog"); FilterFactory ff = CommonFactoryFinder.getFilterFactory(); Class iface = getCatalogInfoInterface(ci.getClass()); CatalogInfo replacement = catalog.get(iface, ff.equal(ff.property("id"), ff.literal(id), true)); return replacement; } /** * Gathers the most specific CatalogInfo sub-interface from the specified class object * @param class1 * */ private Class getCatalogInfoInterface(Class<? extends CatalogInfo> clazz) { Class result = CatalogInfo.class; for (Class c : clazz.getInterfaces()) { if(result.isAssignableFrom(c)) { result = c; } } return result; } /** * Wraps an object in a proxy. * * @throws RuntimeException If creating the proxy fails. */ public static <T> T create( T proxyObject, Class<T> clazz ) { return ProxyUtils.createProxy(proxyObject, clazz, new ModificationProxy( proxyObject )); } /** * 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 ) { return ProxyUtils.unwrap(object, ModificationProxy.class); } /** * 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 ) { return ProxyUtils.handler(object, ModificationProxy.class); } 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 ); }; } }