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("<[?].+?[?]>", "");
+ }
+ }
+
+}