/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 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.*; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.beanutils.ConstructorUtils; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.SerializationUtils; import org.geoserver.catalog.CatalogInfo; import org.geoserver.config.util.XStreamPersister; import org.geoserver.config.util.XStreamPersisterFactory; import org.geotools.util.logging.Logging; import com.thoughtworks.xstream.XStream; /** * Utility class used to wrap/clone objects and collections by various strategies: * <ul> * <li>Avoid cloning ModificationProxy proxies, as well as any CatalogInfo object</li> * <li>Avoid cloning at all well known objects that are known to be immutable (several classes in * java.lang)</li> * <li>Wrap in ModificatinoProxy any object that is a CatalogInfo</li> * <li>Using {@link Cloneable} if available</li> * <li>Using copy constructors if available</li> * <li>Falling back on XStream serialization if the above fails * * @author Andrea Aime - GeoSolutions * */ class ModificationProxyCloner { private static final XStreamPersisterFactory XSTREAM_PERSISTER_FACTORY = new XStreamPersisterFactory(); static final Logger LOGGER = Logging.getLogger(ModificationProxyCloner.class); static final Map<Class, Class> CATALOGINFO_INTERFACE_CACHE = new ConcurrentHashMap<Class, Class>(); /** * Best effort object cloning utility, tries different lightweight strategies, then falls back * on copy by XStream serialization (we use that one as we have a number of hooks to avoid deep * copying the catalog, and re-attaching to it, in there) * * @param source * */ static <T> T clone(T source) { // null? if (source == null) { return null; } // already a modification proxy? if(ModificationProxy.handler(source) != null) { return source; } // is it a catalog info? if(source instanceof CatalogInfo) { // mumble... shouldn't we wrap this one in a modification proxy object? return (T) ModificationProxy.create(source, getDeepestCatalogInfoInterface((CatalogInfo) source)); } // if a known immutable? if (source instanceof String || source instanceof Byte || source instanceof Short || source instanceof Integer || source instanceof Float || source instanceof Double || source instanceof BigInteger || source instanceof BigDecimal) { return (T) source; } // is it cloneable? try { if (source instanceof Cloneable) { // methodutils does not seem to work against "clone()"... // return (T) MethodUtils.invokeExactMethod(source, "clone", null, null); Method method = source.getClass().getDeclaredMethod("clone"); if(Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == 0) { return (T) method.invoke(source); } } } catch (Exception e) { LOGGER.log( Level.FINE, "Source object is cloneable, yet it does not have a public no argument method 'clone'", e); } // does it have a copy constructor? Constructor copyConstructor = ConstructorUtils.getAccessibleConstructor(source.getClass(), source.getClass()); if (copyConstructor != null) { try { return (T) copyConstructor.newInstance(source); } catch (Exception e) { LOGGER.log(Level.FINE, "Source has a copy constructor, but it failed, skipping to XStream", e); } } if(source instanceof Serializable) { return (T) cloneSerializable((Serializable)source); } else { XStreamPersister persister = XSTREAM_PERSISTER_FACTORY.createXMLPersister(); XStream xs = persister.getXStream(); String xml = xs.toXML(source); T copy = (T) xs.fromXML(xml); return copy; } } static <T extends Serializable> T cloneSerializable(T source) { byte[] bytes = SerializationUtils.serialize(source); try { ObjectInputStream input = new ModProxyObjectInputStream(new ByteArrayInputStream(bytes)); return (T) input.readObject(); } catch (Exception e) { throw new RuntimeException("Error cloning serializable object", e); } } static Class getDeepestCatalogInfoInterface(CatalogInfo object) { Class<? extends CatalogInfo> sourceClass = object.getClass(); Class result = CATALOGINFO_INTERFACE_CACHE.get(sourceClass); if(result == null) { List<Class<?>> interfaces = ClassUtils.getAllInterfaces(sourceClass); // collect only CatalogInfo related interfaces List<Class> cis = new ArrayList<Class>(); for (Class clazz : interfaces) { if(CatalogInfo.class.isAssignableFrom(clazz)) { cis.add(clazz); } } if(cis.size() == 0) { result = null; } else if(cis.size() == 1) { result = cis.get(0); } else { Collections.sort(cis, new Comparator<Class>() { @Override public int compare(Class c1, Class c2) { if(c1.isAssignableFrom(c2)) { return 1; } else if(c2.isAssignableFrom(c1)) { return -1; } else { return 0; } } }); result = cis.get(0); } CATALOGINFO_INTERFACE_CACHE.put(sourceClass, result); } return result; } /** * Shallow or deep copies the provided collection * * @param source * @param deepCopy If true, a deep copy will be done, otherwise the cloned collection will * contain the exact same objects as the source * * @throws InstantiationException * @throws IllegalAccessException */ public static <T> Collection<T> cloneCollection(Collection<T> source, boolean deepCopy) throws InstantiationException, IllegalAccessException { if (source == null) { // nothing to copy return null; } Collection<T> copy; try { copy = source.getClass().newInstance(); } catch (InstantiationException e) { //we'll just pick something if (source instanceof Set) { copy = new HashSet<T>(); } else { copy = new ArrayList<T>(); } } if (deepCopy) { for (T object : source) { T objectCopy = clone(object); copy.add(objectCopy); } } else { copy.addAll(source); } return copy; } /** * Shallow or deep copies the provided collection * * @param <K> * @param <V> * * @param source * @param deepCopy If true, a deep copy will be done, otherwise the cloned collection will * contain the exact same objects as the source * * @throws InstantiationException * @throws IllegalAccessException */ public static <K, V> Map<K, V> cloneMap(Map<K, V> source, boolean deepCopy) throws InstantiationException, IllegalAccessException { if (source == null) { // nothing to copy return null; } Map<K, V> copy = source.getClass().newInstance(); if (deepCopy) { for (Map.Entry<K, V> entry : source.entrySet()) { K keyCopy = clone(entry.getKey()); V valueCopy = clone(entry.getValue()); copy.put(keyCopy, valueCopy); } } else { copy.putAll(source); } return copy; } /** * Custom object output stream used to ensure a stable class loader used. */ static class ModProxyObjectInputStream extends ObjectInputStream { ClassLoader classLoader; public ModProxyObjectInputStream(InputStream input) throws IOException { super(input); this.classLoader = ModificationProxy.class.getClassLoader(); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); try { return Class.forName(name, false, classLoader); } catch (ClassNotFoundException ex) { return super.resolveClass(desc); } } } }