Package org.geoserver.catalog.impl

Source Code of org.geoserver.catalog.impl.ModificationProxy

/* (c) 2014 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.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.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 = 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 );

            //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;
    }

    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
     * @return
     */
    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 );
        };
    }
}
TOP

Related Classes of org.geoserver.catalog.impl.ModificationProxy

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.