diff --git a/bom/pom.xml b/bom/pom.xml index 8f2fb973dd42..a165a18f2a04 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -332,9 +332,9 @@ THE SOFTWARE. - org.jvnet.hudson + com.thoughtworks.xstream xstream - 1.4.7-jenkins-1 + 1.4.13 net.sf.kxml diff --git a/core/pom.xml b/core/pom.xml index 561ff058a7ac..d8048b8b5f55 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -230,7 +230,7 @@ THE SOFTWARE. antlr - org.jvnet.hudson + com.thoughtworks.xstream xstream diff --git a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java index 3cc2520c11c0..d0d039c0e7d7 100644 --- a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java +++ b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java @@ -27,6 +27,7 @@ import com.thoughtworks.xstream.converters.UnmarshallingContext; import hudson.Extension; import hudson.ExtensionList; +import hudson.Main; import hudson.XmlFile; import hudson.model.AdministrativeMonitor; import hudson.model.Item; @@ -207,6 +208,9 @@ public static void report(Saveable obj, Collection errors) { if (e instanceof ReportException) { report(obj, ((ReportException)e).version); } else { + if (Main.isUnitTest) { + LOGGER.log(Level.INFO, "Trouble loading " + obj, e); + } if (++i > 1) buf.append(", "); buf.append(e.getClass().getSimpleName()).append(": ").append(e.getMessage()); } diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 6af15f3ad15d..47cb5976db8d 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -614,7 +614,7 @@ public final XmlFile getConfigFile() { return Items.getConfigFile(this); } - private Object writeReplace() { + protected Object writeReplace() { return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this)); } private static class Replacer { diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java index b40df81887ac..c37bb9a3cd09 100644 --- a/core/src/main/java/hudson/model/ListView.java +++ b/core/src/main/java/hudson/model/ListView.java @@ -132,7 +132,7 @@ public void setJobFilters(List jobFilters) throws IOException { this.jobFilters.replaceBy(jobFilters); } - private Object readResolve() { + protected Object readResolve() { if(includeRegex!=null) { try { includePattern = Pattern.compile(includeRegex); diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index afdb68a5f11e..ea8388f1f94d 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -2383,7 +2383,7 @@ public Api getApi() throws AccessDeniedException { } } - private Object readResolve() { + protected Object readResolve() { this.future = new FutureImpl(task); return this; } diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 825640440098..c148af214a20 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -2082,7 +2082,7 @@ public synchronized void save() throws IOException { return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml")); } - private Object writeReplace() { + protected Object writeReplace() { return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this)); } private static class Replacer { diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java index 438c51356220..c7c10d5db0c2 100644 --- a/core/src/main/java/hudson/tasks/Maven.java +++ b/core/src/main/java/hudson/tasks/Maven.java @@ -553,7 +553,7 @@ public void buildEnvVars(EnvVars env) { public boolean meetsMavenReqVersion(Launcher launcher, int mavenReqVersion) throws IOException, InterruptedException { // FIXME using similar stuff as in the maven plugin could be better // olamy : but will add a dependency on maven in core -> so not so good - String mavenVersion = launcher.getChannel().call(new GetMavenVersion()); + String mavenVersion = launcher.getChannel().call(new GetMavenVersion(getHome())); if (!mavenVersion.equals("")) { if (mavenReqVersion == MAVEN_20) { @@ -572,11 +572,14 @@ else if (mavenReqVersion == MAVEN_30) { return false; } - private class GetMavenVersion extends MasterToSlaveCallable { - private static final long serialVersionUID = -4143159957567745621L; + private static class GetMavenVersion extends MasterToSlaveCallable { + private final String home; + GetMavenVersion(String home) { + this.home = home; + } @Override public String call() throws IOException { - File[] jars = new File(getHomeDir(), "lib").listFiles(); + File[] jars = new File(home, "lib").listFiles(); if (jars != null) { // be defensive for (File jar : jars) { if (jar.getName().startsWith("maven-")) { @@ -608,24 +611,29 @@ public boolean isMaven2_1(Launcher launcher) throws IOException, InterruptedExce * Gets the executable path of this maven on the given target system. */ public String getExecutable(Launcher launcher) throws IOException, InterruptedException { - return launcher.getChannel().call(new GetExecutable()); - } - private class GetExecutable extends MasterToSlaveCallable { - private static final long serialVersionUID = 2373163112639943768L; - @Override - public String call() throws IOException { - File exe = getExeFile("mvn"); - if(exe.exists()) - return exe.getPath(); - exe = getExeFile("maven"); - if(exe.exists()) - return exe.getPath(); - return null; + return launcher.getChannel().call(new GetExecutable(getHome())); + } + private static class GetExecutable extends MasterToSlaveCallable { + private final String rawHome; + GetExecutable(String rawHome) { + this.rawHome = rawHome; + } + @Override + public String call() throws IOException { + File exe = getExeFile("mvn", rawHome); + if (exe.exists()) { + return exe.getPath(); + } + exe = getExeFile("maven", rawHome); + if (exe.exists()) { + return exe.getPath(); } + return null; + } } - private File getExeFile(String execName) { - String m2Home = Util.replaceMacro(getHome(),EnvVars.masterEnvVars); + private static File getExeFile(String execName, String home) { + String m2Home = Util.replaceMacro(home, EnvVars.masterEnvVars); if(Functions.isWindows()) { File exeFile = new File(m2Home, "bin/" + execName + ".bat"); diff --git a/core/src/main/java/hudson/tools/ToolInstallation.java b/core/src/main/java/hudson/tools/ToolInstallation.java index df7e30f69f26..8752ea7d2dfa 100644 --- a/core/src/main/java/hudson/tools/ToolInstallation.java +++ b/core/src/main/java/hudson/tools/ToolInstallation.java @@ -30,19 +30,26 @@ import hudson.ExtensionPoint; import hudson.diagnosis.OldDataMonitor; import hudson.model.*; +import hudson.remoting.Channel; import hudson.slaves.NodeSpecific; import hudson.util.DescribableList; import hudson.util.XStream2; import java.io.Serializable; +import java.io.StringReader; import java.io.IOException; import java.util.List; -import com.thoughtworks.xstream.annotations.XStreamSerializable; import com.thoughtworks.xstream.converters.UnmarshallingContext; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.logging.Level; +import java.util.logging.Logger; import jenkins.model.Jenkins; +import jenkins.util.Timer; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; /** * Formalization of a tool installed in nodes used for builds. @@ -76,14 +83,16 @@ * @since 1.286 */ public abstract class ToolInstallation extends AbstractDescribableImpl implements Serializable, ExtensionPoint { + + private static final Logger LOGGER = Logger.getLogger(ToolInstallation.class.getName()); + private final String name; private /*almost final*/ String home; /** * {@link ToolProperty}s that are associated with this tool. */ - @XStreamSerializable - private transient /*almost final*/ DescribableList,ToolPropertyDescriptor> properties + private /*almost final*/ DescribableList,ToolPropertyDescriptor> properties = new DescribableList<>(Saveable.NOOP); /** @@ -145,8 +154,10 @@ public String getName() { public void buildEnvVars(EnvVars env) { } - public DescribableList,ToolPropertyDescriptor> getProperties() { - assert properties!=null; + public synchronized DescribableList,ToolPropertyDescriptor> getProperties() { + if (properties == null) { + properties = new DescribableList<>(Saveable.NOOP); + } return properties; } @@ -210,13 +221,32 @@ protected String translateFor(Node node, TaskListener log) throws IOException, I * Invoked by XStream when this object is read into memory. */ protected Object readResolve() { - if(properties==null) - properties = new DescribableList<>(Saveable.NOOP); - for (ToolProperty p : properties) - _setTool(p, this); + if (properties != null) { + for (ToolProperty p : properties) { + _setTool(p, this); + } + } return this; } + protected Object writeReplace() throws Exception { + if (Channel.current() == null) { // XStream + return this; + } else { // Remoting + LOGGER.log(Level.WARNING, "Serialization of " + getClass().getSimpleName() + " extends ToolInstallation over Remoting is deprecated", new Throwable()); + // Hack: properties is not serializable, so try to serialize as XML (in another thread); delete ; deserialize; then load a clone + String xml1 = Timer.get().submit(() -> Jenkins.XSTREAM2.toXML(this)).get(); + Document dom = new SAXReader().read(new StringReader(xml1)); + Element properties = dom.getRootElement().element("properties"); + if (properties != null) { + dom.getRootElement().remove(properties); + } + String xml2 = dom.asXML(); + ToolInstallation clone = (ToolInstallation) Timer.get().submit(() -> Jenkins.XSTREAM2.fromXML(xml2)).get(); + return clone; + } + } + @Override public String toString() { return getClass().getSimpleName() + "[" + name + "]"; } @@ -244,5 +274,4 @@ public static DescriptorExtensionList> all() return Jenkins.get().getDescriptorList(ToolInstallation.class); } - private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/hudson/util/PersistedList.java b/core/src/main/java/hudson/util/PersistedList.java index 78373c34c6bb..acbddee330e5 100644 --- a/core/src/main/java/hudson/util/PersistedList.java +++ b/core/src/main/java/hudson/util/PersistedList.java @@ -23,6 +23,7 @@ */ package hudson.util; +import com.google.common.collect.ImmutableSet; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; @@ -40,6 +41,10 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Collection whose change is notified to the parent object for persistence. @@ -48,6 +53,9 @@ * @since 1.333 */ public class PersistedList extends AbstractList { + + private static final Logger LOGGER = Logger.getLogger(PersistedList.class.getName()); + protected final CopyOnWriteList data = new CopyOnWriteList<>(); protected Saveable owner = Saveable.NOOP; @@ -170,7 +178,30 @@ public Iterator iterator() { * Called when a list is mutated. */ protected void onModified() throws IOException { - owner.save(); + try { + owner.save(); + } catch (IOException x) { + Optional ignored = stream().filter(PersistedList::ignoreSerializationErrors).findAny(); + if (ignored.isPresent()) { + LOGGER.log(Level.WARNING, "Ignoring serialization errors in " + ignored.get() + "; update your parent POM to 4.8 or newer", x); + } else { + throw x; + } + } + } + + // TODO until https://github.com/jenkinsci/jenkins-test-harness/pull/243 is widely adopted: + private static final Set IGNORED_CLASSES = ImmutableSet.of("org.jvnet.hudson.test.TestBuilder", "org.jvnet.hudson.test.TestNotifier"); + // (SingleFileSCM & ExtractResourceWithChangesSCM would also be nice to suppress, but they are not kept in a PersistedList.) + private static boolean ignoreSerializationErrors(Object o) { + if (o != null) { + for (Class c = o.getClass(); c != Object.class; c = c.getSuperclass()) { + if (IGNORED_CLASSES.contains(c.getName())) { + return true; + } + } + } + return false; } /** diff --git a/core/src/main/java/hudson/util/RobustCollectionConverter.java b/core/src/main/java/hudson/util/RobustCollectionConverter.java index 7f95b2e873a4..4e0d01b15c1f 100644 --- a/core/src/main/java/hudson/util/RobustCollectionConverter.java +++ b/core/src/main/java/hudson/util/RobustCollectionConverter.java @@ -31,6 +31,7 @@ import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; +import com.thoughtworks.xstream.core.ClassLoaderReference; import java.util.Collection; import java.util.concurrent.CopyOnWriteArrayList; @@ -48,6 +49,7 @@ * * @author Kohsuke Kawaguchi */ +@SuppressWarnings({"rawtypes", "unchecked"}) public class RobustCollectionConverter extends CollectionConverter { private final SerializableConverter sc; @@ -57,7 +59,7 @@ public RobustCollectionConverter(XStream xs) { public RobustCollectionConverter(Mapper mapper, ReflectionProvider reflectionProvider) { super(mapper); - sc = new SerializableConverter(mapper,reflectionProvider); + sc = new SerializableConverter(mapper, reflectionProvider, new ClassLoaderReference(null)); } @Override @@ -82,7 +84,7 @@ protected void populateCollection(HierarchicalStreamReader reader, Unmarshalling while (reader.hasMoreChildren()) { reader.moveDown(); try { - Object item = readItem(reader, context, collection); + Object item = readBareItem(reader, context, collection); collection.add(item); } catch (CriticalXStreamException e) { throw e; diff --git a/core/src/main/java/hudson/util/RobustMapConverter.java b/core/src/main/java/hudson/util/RobustMapConverter.java index ce16bc0b1e4f..c83e7c8fe819 100644 --- a/core/src/main/java/hudson/util/RobustMapConverter.java +++ b/core/src/main/java/hudson/util/RobustMapConverter.java @@ -55,7 +55,7 @@ final class RobustMapConverter extends MapConverter { private Object read(HierarchicalStreamReader reader, UnmarshallingContext context, Map map) { reader.moveDown(); try { - return readItem(reader, context, map); + return readBareItem(reader, context, map); } catch (CriticalXStreamException x) { throw x; } catch (XStreamException | LinkageError x) { diff --git a/core/src/main/java/hudson/util/RobustReflectionConverter.java b/core/src/main/java/hudson/util/RobustReflectionConverter.java index 9b07e3188640..dddf70fd082e 100644 --- a/core/src/main/java/hudson/util/RobustReflectionConverter.java +++ b/core/src/main/java/hudson/util/RobustReflectionConverter.java @@ -33,9 +33,8 @@ import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; -import com.thoughtworks.xstream.converters.reflection.SerializationMethodInvoker; import com.thoughtworks.xstream.core.util.Primitives; -import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper; +import com.thoughtworks.xstream.core.util.SerializationMembers; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.mapper.Mapper; @@ -71,11 +70,12 @@ * * */ +@SuppressWarnings({"rawtypes", "unchecked"}) public class RobustReflectionConverter implements Converter { protected final ReflectionProvider reflectionProvider; protected final Mapper mapper; - protected transient SerializationMethodInvoker serializationMethodInvoker; + protected transient SerializationMembers serializationMethodInvoker; private transient ReflectionProvider pureJavaReflectionProvider; private final @NonNull XStream2.ClassOwnership classOwnership; /** There are typically few critical fields around, but we end up looking up in this map a lot. @@ -95,7 +95,7 @@ public RobustReflectionConverter(Mapper mapper, ReflectionProvider reflectionPro this.reflectionProvider = reflectionProvider; assert classOwnership != null; this.classOwnership = classOwnership; - serializationMethodInvoker = new SerializationMethodInvoker(); + serializationMethodInvoker = new SerializationMembers(); } void addCriticalField(Class clazz, String field) { @@ -189,6 +189,8 @@ protected void doMarshal(final Object source, final HierarchicalStreamWriter wri // Attributes might be preferred to child elements ... reflectionProvider.visitSerializableFields(source, new ReflectionProvider.Visitor() { + @SuppressWarnings("deprecation") // deliberately calling deprecated methods? + @Override public void visit(String fieldName, Class type, Class definedIn, Object value) { SingleValueConverter converter = mapper.getConverterFromItemType(fieldName, type, definedIn); if (converter == null) converter = mapper.getConverterFromItemType(fieldName, type); @@ -226,12 +228,13 @@ public void visit(String fieldName, Class fieldType, Class definedIn, Object new } } + @SuppressWarnings("deprecation") // TODO HierarchicalStreamWriter#startNode(String, Class) in 1.5.0 private void writeField(String fieldName, String aliasName, Class fieldType, Class definedIn, Object newObj) { try { if (!mapper.shouldSerializeMember(definedIn, aliasName)) { return; } - ExtendedHierarchicalStreamWriterHelper.startNode(writer, mapper.serializedMember(definedIn, aliasName), fieldType); + com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper.startNode(writer, mapper.serializedMember(definedIn, aliasName), fieldType); Class actualType = newObj.getClass(); @@ -288,7 +291,7 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re SingleValueConverter converter = mapper.getConverterFromAttribute(field.getDeclaringClass(),attrName,field.getType()); Class type = field.getType(); if (converter == null) { - converter = mapper.getConverterFromItemType(type); + converter = mapper.getConverterFromItemType(type); // TODO add fieldName & definedIn args } if (converter != null) { Object value = converter.fromString(reader.getAttribute(attrAlias)); @@ -492,7 +495,7 @@ private Class determineType(HierarchicalStreamReader reader, boolean validField, } private Object readResolve() { - serializationMethodInvoker = new SerializationMethodInvoker(); + serializationMethodInvoker = new SerializationMembers(); return this; } diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java index c86e82308ec0..2fe7fd3082bd 100644 --- a/core/src/main/java/hudson/util/XStream2.java +++ b/core/src/main/java/hudson/util/XStream2.java @@ -27,10 +27,8 @@ import com.google.common.collect.ImmutableMap; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.KXml2Driver; -import com.thoughtworks.xstream.mapper.AnnotationMapper; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; -import com.thoughtworks.xstream.mapper.XStream11XmlFriendlyMapper; import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.ConverterMatcher; @@ -40,6 +38,7 @@ import com.thoughtworks.xstream.converters.SingleValueConverterWrapper; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.converters.extended.DynamicProxyConverter; +import com.thoughtworks.xstream.core.ClassLoaderReference; import com.thoughtworks.xstream.core.JVM; import com.thoughtworks.xstream.core.util.Fields; import com.thoughtworks.xstream.io.HierarchicalStreamDriver; @@ -47,6 +46,7 @@ import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.ReaderWrapper; import com.thoughtworks.xstream.mapper.CannotResolveClassException; +import com.thoughtworks.xstream.security.AnyTypePermission; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.XmlFile; @@ -82,8 +82,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; /** - * {@link XStream} enhanced for additional Java5 support and improved robustness. - * @author Kohsuke Kawaguchi + * {@link XStream} customized in various ways for Jenkins’ needs. + * Most importantly, integrates {@link RobustReflectionConverter}. */ public class XStream2 extends XStream { @@ -212,10 +212,11 @@ public void moveDown() { } @Override - protected Converter createDefaultConverter() { + protected void setupConverters() { + super.setupConverters(); // replace default reflection converter - reflectionConverter = new RobustReflectionConverter(getMapper(),new JVM().bestReflectionProvider(), new PluginClassOwnership()); - return reflectionConverter; + reflectionConverter = new RobustReflectionConverter(getMapper(), JVM.newReflectionProvider(), new PluginClassOwnership()); + registerConverter(reflectionConverter, PRIORITY_VERY_LOW + 1); } /** @@ -235,7 +236,7 @@ static String trimVersion(String version) { private void init() { // list up types that should be marshalled out like a value, without referential integrity tracking. - addImmutableType(Result.class); + addImmutableType(Result.class, false); // http://www.openwall.com/lists/oss-security/2017/04/03/4 denyTypes(new Class[] { void.class, Void.class }); @@ -257,8 +258,9 @@ private void init() { registerConverter(new AssociatedConverterImpl(this), -10); registerConverter(new BlacklistedTypesConverter(), PRIORITY_VERY_HIGH); // SECURITY-247 defense + addPermission(AnyTypePermission.ANY); // covered by JEP-200, avoid securityWarningGiven - registerConverter(new DynamicProxyConverter(getMapper()) { // SECURITY-105 defense + registerConverter(new DynamicProxyConverter(getMapper(), new ClassLoaderReference(getClassLoader())) { // SECURITY-105 defense @Override public boolean canConvert(Class type) { return /* this precedes NullConverter */ type != null && super.canConvert(type); } @@ -281,11 +283,8 @@ else if (type != null && ImmutableList.class.isAssignableFrom(type)) return super.serializedClass(type); } }); - AnnotationMapper a = new AnnotationMapper(m, getConverterRegistry(), getConverterLookup(), getClassLoader(), getReflectionProvider(), getJvm()); - // TODO JENKINS-19561 this is unsafe: - a.autodetectAnnotations(true); - mapperInjectionPoint = new MapperInjectionPoint(a); + mapperInjectionPoint = new MapperInjectionPoint(m); return mapperInjectionPoint; } @@ -361,7 +360,7 @@ public void addCompatibilityAlias(String oldClassName, Class newClass) { /** * 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 {@link XStream11XmlFriendlyMapper}. + * for old serialized data was maintained via {@link com.thoughtworks.xstream.mapper.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. */ diff --git a/core/src/main/java/hudson/util/xstream/MapperDelegate.java b/core/src/main/java/hudson/util/xstream/MapperDelegate.java index f7eb78b5516e..cb84d40803b6 100644 --- a/core/src/main/java/hudson/util/xstream/MapperDelegate.java +++ b/core/src/main/java/hudson/util/xstream/MapperDelegate.java @@ -150,4 +150,15 @@ public SingleValueConverter getConverterFromAttribute(Class type, String attribu public SingleValueConverter getConverterFromAttribute(Class definedIn, String attribute, Class type) { return delegate.getConverterFromAttribute(definedIn, attribute, type); } + + @Override + public boolean isIgnoredElement(String name) { + return delegate.isIgnoredElement(name); + } + + @Override + public boolean isReferenceable(Class type) { + return delegate.isReferenceable(type); + } + } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 1b6fad027d0b..2e473e77f61f 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -1032,7 +1032,7 @@ protected void doRun() throws Exception { * Maintains backwards compatibility. Invoked by XStream when this object is de-serialized. */ @SuppressWarnings({"unused"}) - private Object readResolve() { + protected Object readResolve() { if (jdks == null) { jdks = new ArrayList<>(); } diff --git a/core/src/test/java/hudson/util/XStream2Test.java b/core/src/test/java/hudson/util/XStream2Test.java index ec9e0bbbbb44..e32f5d3e26e3 100644 --- a/core/src/test/java/hudson/util/XStream2Test.java +++ b/core/src/test/java/hudson/util/XStream2Test.java @@ -34,6 +34,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.thoughtworks.xstream.XStreamException; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.mapper.CannotResolveClassException; import com.thoughtworks.xstream.security.ForbiddenClassException; import hudson.model.Result; import hudson.model.Run; @@ -46,6 +48,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; +import static org.hamcrest.Matchers.instanceOf; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -530,9 +533,42 @@ public void setListDefaultNull(List listDefaultNull) { public void crashXstream() throws Exception { try { new XStream2().fromXML(""); - fail("expected to throw ForbiddenClassException, but why are we still alive?"); - } catch (ForbiddenClassException ex) { + fail("expected to throw ConversionException, but why are we still alive?"); + } catch (XStreamException ex) { // pass } } + + @Test + public void annotations() throws Exception { + assertEquals("not registered, so sorry", "", Jenkins.XSTREAM2.toXML(new C1())); + assertEquals("manually registered", "", Jenkins.XSTREAM2.toXML(new C2())); + assertEquals("manually processed", "", Jenkins.XSTREAM2.toXML(new C3())); + try { + Jenkins.XSTREAM2.fromXML(""); + fail("this could never have worked anyway"); + } catch (CannotResolveClassException x) { + // OK + } + Jenkins.XSTREAM2.processAnnotations(C5.class); + assertThat("can deserialize from annotations so long as the processing happened at some point", Jenkins.XSTREAM2.fromXML(""), instanceOf(C5.class)); + } + @XStreamAlias("C-1") + public static final class C1 {} + public static final class C2 { + static { + Jenkins.XSTREAM2.alias("C-2", C2.class); + } + } + @XStreamAlias("C-3") + public static final class C3 { + static { + Jenkins.XSTREAM2.processAnnotations(C3.class); + } + } + @XStreamAlias("C-4") + public static final class C4 {} + @XStreamAlias("C-5") + public static final class C5 {} + } diff --git a/pom.xml b/pom.xml index fcf29a5419be..7625b4fdd633 100755 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ THE SOFTWARE. https://api.github.com jenkins-jira - 2.6.2 + 2.6.4 1.17 0.11 ${skipTests} diff --git a/test/pom.xml b/test/pom.xml index df04e68f36e3..922592ad30ba 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -51,6 +51,11 @@ THE SOFTWARE. pom import + + org.jenkins-ci.plugins + ant + 1.11 + @@ -64,7 +69,7 @@ THE SOFTWARE. ${project.groupId} jenkins-test-harness - 2.63 + 2.68 test diff --git a/test/src/test/java/hudson/model/ProjectTest.java b/test/src/test/java/hudson/model/ProjectTest.java index 137443631ffd..d2e802883be8 100644 --- a/test/src/test/java/hudson/model/ProjectTest.java +++ b/test/src/test/java/hudson/model/ProjectTest.java @@ -256,7 +256,7 @@ public void testGetScmCheckoutRetryCount() throws Exception{ HtmlForm form = j.createWebClient().goTo(p.getUrl() + "/configure").getFormByName("config"); ((HtmlElement)form.getByXPath("//div[@class='advancedLink']//button").get(0)).click(); // required due to the new default behavior of click - form.getInputByName("hasCustomScmCheckoutRetryCount").click(new Event(), true); + form.getInputByName("hasCustomScmCheckoutRetryCount").click(new Event(), false, false, false, true); form.getInputByName("scmCheckoutRetryCount").setValueAttribute("7"); j.submit(form); assertEquals("Scm retry count was set.", 7, p.getScmCheckoutRetryCount()); diff --git a/test/src/test/java/hudson/util/RobustReflectionConverterTest.java b/test/src/test/java/hudson/util/RobustReflectionConverterTest.java index 892835170954..ad0160830a8d 100644 --- a/test/src/test/java/hudson/util/RobustReflectionConverterTest.java +++ b/test/src/test/java/hudson/util/RobustReflectionConverterTest.java @@ -81,7 +81,7 @@ public class RobustReflectionConverterTest { Map data = odm.getData(); assertEquals(Collections.singleton(p), data.keySet()); String text = data.values().iterator().next().extra; - assertTrue(text, text.contains("Could not call hudson.triggers.TimerTrigger.readResolve")); + assertTrue(text, text.contains("hudson.triggers.TimerTrigger.readResolve")); } // Testing describable object to demonstrate what is expected with RobustReflectionConverter#addCriticalField diff --git a/test/src/test/java/hudson/util/XStream2AnnotationTest.java b/test/src/test/java/hudson/util/XStream2AnnotationTest.java new file mode 100644 index 000000000000..dc9f7bf25930 --- /dev/null +++ b/test/src/test/java/hudson/util/XStream2AnnotationTest.java @@ -0,0 +1,122 @@ +/* + * The MIT License + * + * Copyright 2020 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.util; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import hudson.ExtensionList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import jenkins.model.GlobalConfiguration; +import org.apache.commons.io.FileUtils; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import org.junit.Test; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsSessionRule; +import org.jvnet.hudson.test.TestExtension; + +public class XStream2AnnotationTest { + + @Rule + public JenkinsSessionRule rr = new JenkinsSessionRule(); + + @Test + public void xStreamAlias() throws Throwable { + rr.then(r -> { + AnnotatedProcessed annotatedProcessed = AnnotatedProcessed.get(); + annotatedProcessed.x = 1; + annotatedProcessed.save(); + assertThat(annotatedProcessed.xml(), is("1")); + AnnotatedUnprocessed annotatedUnprocessed = AnnotatedUnprocessed.get(); + annotatedUnprocessed.x = 2; + annotatedUnprocessed.save(); + assertThat(annotatedUnprocessed.xml(), is("2")); + Programmatic programmatic = Programmatic.get(); + programmatic.x = 3; + programmatic.save(); + assertThat(programmatic.xml(), is("3")); + }); + rr.then(r -> { + assertThat(AnnotatedProcessed.get().x, is(1)); + assertThat(AnnotatedUnprocessed.get().x, is(2)); + assertThat(Programmatic.get().x, is(3)); + // Typical content saved by Jenkins session when annotation autodetection was still enabled: + AnnotatedUnprocessed.get().writeXml("4"); + }); + rr.then(r -> { + assertThat("CannotResolveClassException/IOException caught in Descriptor.load", AnnotatedUnprocessed.get().x, is(0)); + }); + } + + @XStreamAlias("myconf-annotated-processed") + @TestExtension("xStreamAlias") + public static class AnnotatedProcessed extends GlobalConfiguration { + static AnnotatedProcessed get() { + return ExtensionList.lookupSingleton(AnnotatedProcessed.class); + } + int x; + public AnnotatedProcessed() { + getConfigFile().getXStream().processAnnotations(AnnotatedProcessed.class); + load(); + } + String xml() throws IOException { + return getConfigFile().asString().replaceAll("\n *", "").replaceAll("<[?].+?[?]>", ""); + } + } + + @XStreamAlias("myconf-annotated-unprocessed") + @TestExtension("xStreamAlias") + public static class AnnotatedUnprocessed extends GlobalConfiguration { + static AnnotatedUnprocessed get() { + return ExtensionList.lookupSingleton(AnnotatedUnprocessed.class); + } + int x; + public AnnotatedUnprocessed() { + load(); + } + String xml() throws IOException { + return getConfigFile().asString().replaceAll("\n *", "").replaceAll("<[?].+?[?]>", ""); + } + void writeXml(String xml) throws IOException { + FileUtils.write(getConfigFile().getFile(), xml, StandardCharsets.UTF_8); + } + } + + @TestExtension("xStreamAlias") + public static class Programmatic extends GlobalConfiguration { + static Programmatic get() { + return ExtensionList.lookupSingleton(Programmatic.class); + } + int x; + public Programmatic() { + getConfigFile().getXStream().alias("myconf-programmatic", Programmatic.class); + load(); + } + String xml() throws IOException { + return getConfigFile().asString().replaceAll("\n *", "").replaceAll("<[?].+?[?]>", ""); + } + } + +}