/** * ***************************************************************************** * * Copyright (c) 2004-2010 Oracle Corporation. * * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Alan Harder * * ****************************************************************************** */ package hudson.util; import com.google.common.collect.ImmutableMap; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.ConverterMatcher; import com.thoughtworks.xstream.converters.DataHolder; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.SingleValueConverter; import com.thoughtworks.xstream.converters.SingleValueConverterWrapper; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.core.JVM; import com.thoughtworks.xstream.io.HierarchicalStreamDriver; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.mapper.CannotResolveClassException; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; import hudson.diagnosis.OldDataMonitor; import hudson.model.Hudson; import hudson.model.Label; import hudson.model.Result; import hudson.model.Saveable; import hudson.util.xstream.ImmutableMapConverter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.ConcurrentHashMap; /** * {@link XStream} enhanced for additional Java5 support and improved * robustness. * * @author Kohsuke Kawaguchi */ public class XStream2 extends XStream { private Converter reflectionConverter; private ThreadLocal<Boolean> oldData = new ThreadLocal<Boolean>(); public XStream2() { init(); } public XStream2(HierarchicalStreamDriver hierarchicalStreamDriver) { super(hierarchicalStreamDriver); init(); } @Override public Object unmarshal(HierarchicalStreamReader reader, Object root, DataHolder dataHolder) { // init() is too early to do this // defensive because some use of XStream happens before plugins are initialized. Hudson h = Hudson.getInstance(); if (h != null && h.pluginManager != null && h.pluginManager.uberClassLoader != null) { setClassLoader(h.pluginManager.uberClassLoader); } Object o = super.unmarshal(reader, root, dataHolder); if (oldData.get() != null) { oldData.remove(); if (o instanceof Saveable) { OldDataMonitor.report((Saveable) o, "1.106"); } } return o; } private void init() { // list up types that should be marshalled out like a value, without referencial integrity tracking. addImmutableType(Result.class); registerConverter(new RobustCollectionConverter(getMapper(), getReflectionProvider()), XStream.PRIORITY_NORMAL); registerConverter(new ImmutableMapConverter(getMapper(), getReflectionProvider()), XStream.PRIORITY_NORMAL); registerConverter(new ConcurrentHashMapConverter(getMapper(), getReflectionProvider()), XStream.PRIORITY_NORMAL); registerConverter(new CopyOnWriteMap.Tree.ConverterImpl(getMapper()), XStream.PRIORITY_NORMAL); // needs to override MapConverter registerConverter(new DescribableList.ConverterImpl(getMapper()), XStream.PRIORITY_NORMAL); // explicitly added to handle subtypes registerConverter(new Label.ConverterImpl(), XStream.PRIORITY_NORMAL); // this should come after all the XStream's default simpler converters, // but before reflection-based one kicks in. registerConverter(new AssociatedConverterImpl(this), XStream.PRIORITY_LOW); reflectionConverter = new RobustReflectionConverter(getMapper(), new JVM().bestReflectionProvider()); registerConverter(reflectionConverter, XStream.PRIORITY_VERY_LOW); } @Override protected MapperWrapper wrapMapper(MapperWrapper next) { MapperWrapper m = new CompatibilityMapper(new MapperWrapper(next) { @Override public String serializedClass(Class type) { if (type != null && ImmutableMap.class.isAssignableFrom(type)) { return super.serializedClass(ImmutableMap.class); } else { return super.serializedClass(type); } } }); // XStream already sets it in 1.4.8 // AnnotationMapper a = new AnnotationMapper(m, getConverterRegistry(), getClassLoader(), getReflectionProvider(), getJvm()); // a.autodetectAnnotations(true); return m; } /** * Prior to Hudson 1.106, XStream 1.1.x was used which encoded "$" in class * names as "-" instead of "_-" that is used now. Up through Hudson 1.348 * compatibility for old serialized data was maintained via * {@code XStream11XmlFriendlyMapper}. However, it was found (HUDSON-5768) * that this caused fields with "__" to fail deserialization due to double * decoding. Now this class is used for compatibility. */ private class CompatibilityMapper extends MapperWrapper { private CompatibilityMapper(Mapper wrapped) { super(wrapped); } @Override public Class realClass(String elementName) { try { return super.realClass(elementName); } catch (CannotResolveClassException e) { // If a "-" is found, retry with mapping this to "$" if (elementName.indexOf('-') >= 0) { try { Class c = super.realClass(elementName.replace('-', '$')); oldData.set(Boolean.TRUE); return c; } catch (CannotResolveClassException e2) { } } // Throw original exception throw e; } } } /** * If a class defines a nested {@code ConverterImpl} subclass, use that as a * {@link Converter}. Its constructor may have XStream/XStream2 and/or * Mapper parameters (or no params). */ private static final class AssociatedConverterImpl implements Converter { private final XStream xstream; private final ConcurrentHashMap<Class, Converter> cache = new ConcurrentHashMap<Class, Converter>(); private AssociatedConverterImpl(XStream xstream) { this.xstream = xstream; } private Converter findConverter(Class t) { Converter result = cache.get(t); if (result != null) // ConcurrentHashMap does not allow null, so use this object to represent null { return result == this ? null : result; } try { if (t == null || t.getClassLoader() == null) { return null; } Class<?> cl = t.getClassLoader().loadClass(t.getName() + "$ConverterImpl"); Constructor<?> c = cl.getConstructors()[0]; Class<?>[] p = c.getParameterTypes(); Object[] args = new Object[p.length]; for (int i = 0; i < p.length; i++) { if (p[i] == XStream.class || p[i] == XStream2.class) { args[i] = xstream; } else if (p[i] == Mapper.class) { args[i] = xstream.getMapper(); } else { throw new InstantiationError("Unrecognized constructor parameter: " + p[i]); } } ConverterMatcher cm = (ConverterMatcher) c.newInstance(args); result = cm instanceof SingleValueConverter ? new SingleValueConverterWrapper((SingleValueConverter) cm) : (Converter) cm; cache.put(t, result); return result; } catch (ClassNotFoundException e) { cache.put(t, this); // See above.. this object in cache represents null return null; } catch (IllegalAccessException e) { IllegalAccessError x = new IllegalAccessError(); x.initCause(e); throw x; } catch (InstantiationException e) { InstantiationError x = new InstantiationError(); x.initCause(e); throw x; } catch (InvocationTargetException e) { InstantiationError x = new InstantiationError(); x.initCause(e); throw x; } } public boolean canConvert(Class type) { return findConverter(type) != null; } public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { findConverter(source.getClass()).marshal(source, writer, context); } public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { return findConverter(context.getRequiredType()).unmarshal(reader, context); } } /** * Create a nested {@code ConverterImpl} subclass that extends this class to * run some callback code just after a type is unmarshalled by * RobustReflectionConverter. Example: * <pre> public static class ConverterImpl extends XStream2.PassthruConverter<MyType> { * public ConverterImpl(XStream2 xstream) { super(xstream); } * * @Override protected void callback(MyType obj, UnmarshallingContext * context) { ... * </pre> */ public static abstract class PassthruConverter<T> implements Converter { private Converter converter; public PassthruConverter(XStream2 xstream) { converter = xstream.reflectionConverter; } public boolean canConvert(Class type) { // marshal/unmarshal called directly from AssociatedConverterImpl return false; } public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { converter.marshal(source, writer, context); } public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { Object obj = converter.unmarshal(reader, context); callback((T) obj, context); return obj; } protected abstract void callback(T obj, UnmarshallingContext context); } }