implicitDeps = DetachedPluginsUtil.getImpliedDependencies(p.name, p.requiredCore);
if(!implicitDeps.isEmpty()) {
for(PluginWrapper.Dependency dep : implicitDeps) {
if(!p.dependencies.containsKey(dep.shortName)) {
diff --git a/core/src/main/java/jenkins/plugins/DetachedPluginsUtil.java b/core/src/main/java/jenkins/plugins/DetachedPluginsUtil.java
new file mode 100644
index 0000000000..55c9e079c9
--- /dev/null
+++ b/core/src/main/java/jenkins/plugins/DetachedPluginsUtil.java
@@ -0,0 +1,224 @@
+package jenkins.plugins;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import hudson.ClassicPluginStrategy;
+import hudson.PluginWrapper;
+import hudson.util.VersionNumber;
+import jenkins.util.java.JavaUtils;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Dedicated class to handle the logic related to so-called detached plugins.
+ *
+ * Originally, some features were directly in Jenkins core. Over time, more and more features got extracted in dedicated plugins.
+ * Issue is: many plugins had started depending on these features, that now were not in the core anymore.
+ * So the chosen design strategy has been that the jenkins.war would embed these plugins, and automatically install them and mark them as optional dependencies.
+ * This way, older plugins would keep working without having to be modified.
+ *
+ * This code was originally moved from {@link ClassicPluginStrategy}.
+ *
+ * @since TODO
+ */
+@Restricted(NoExternalUse.class)
+public class DetachedPluginsUtil {
+ private static final Logger LOGGER = Logger.getLogger(DetachedPluginsUtil.class.getName());
+
+ /**
+ * Record of which plugins which removed from core and when.
+ */
+ @VisibleForTesting
+ static final List DETACHED_LIST;
+
+ /**
+ * Implicit dependencies that are known to be unnecessary and which must be cut out to prevent a dependency cycle among bundled plugins.
+ */
+ private static final Set BREAK_CYCLES;
+
+ static {
+ try (InputStream is = ClassicPluginStrategy.class.getResourceAsStream("/jenkins/split-plugins.txt")) {
+ DETACHED_LIST = ImmutableList.copyOf(configLines(is).map(line -> {
+ String[] pieces = line.split(" ");
+
+ // defaults to Java 1.0 to install unconditionally if unspecified
+ return new DetachedPluginsUtil.DetachedPlugin(pieces[0],
+ pieces[1] + ".*",
+ pieces[2],
+ pieces.length == 4 ? pieces[3] : "1.0");
+ }).collect(Collectors.toList()));
+ } catch (IOException x) {
+ throw new ExceptionInInitializerError(x);
+ }
+ try (InputStream is = ClassicPluginStrategy.class.getResourceAsStream("/jenkins/split-plugin-cycles.txt")) {
+ BREAK_CYCLES = ImmutableSet.copyOf(configLines(is).collect(Collectors.toSet()));
+ } catch (IOException x) {
+ throw new ExceptionInInitializerError(x);
+ }
+ }
+
+ private DetachedPluginsUtil() {
+ }
+
+ /**
+ * Returns all the plugin dependencies that are implicit based on a particular Jenkins version
+ *
+ * @since 2.0
+ */
+ @Nonnull
+ public static List getImpliedDependencies(String pluginName, String jenkinsVersion) {
+ List out = new ArrayList<>();
+ for (DetachedPlugin detached : getDetachedPlugins()) {
+ // don't fix the dependency for itself, or else we'll have a cycle
+ if (detached.shortName.equals(pluginName)) {
+ continue;
+ }
+ if (BREAK_CYCLES.contains(pluginName + ' ' + detached.shortName)) {
+ LOGGER.log(Level.FINE, "skipping implicit dependency {0} → {1}", new Object[]{pluginName, detached.shortName});
+ continue;
+ }
+ // some earlier versions of maven-hpi-plugin apparently puts "null" as a literal in Hudson-Version. watch out for them.
+ if (jenkinsVersion == null || jenkinsVersion.equals("null") || new VersionNumber(jenkinsVersion).compareTo(detached.splitWhen) <= 0) {
+ out.add(new PluginWrapper.Dependency(detached.shortName + ':' + detached.requiredVersion));
+ LOGGER.log(Level.FINE, "adding implicit dependency {0} → {1} because of {2}",
+ new Object[]{pluginName, detached.shortName, jenkinsVersion});
+ }
+ }
+ return out;
+ }
+
+ /**
+ * Get the list of all plugins that have ever been {@link DetachedPlugin detached} from Jenkins core, applicable to the current Java runtime.
+ *
+ * @return A {@link List} of {@link DetachedPlugin}s.
+ * @see JavaUtils#getCurrentJavaRuntimeVersionNumber()
+ */
+ public static @Nonnull
+ List getDetachedPlugins() {
+ return DETACHED_LIST.stream()
+ .filter(plugin -> JavaUtils.getCurrentJavaRuntimeVersionNumber().isNewerThanOrEqualTo(plugin.getMinimumJavaVersion()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get the list of plugins that have been detached since a specific Jenkins release version.
+ *
+ * @param since The Jenkins version.
+ * @return A {@link List} of {@link DetachedPlugin}s.
+ * @see #getDetachedPlugins()
+ */
+ public static @Nonnull
+ List getDetachedPlugins(@Nonnull VersionNumber since) {
+ return getDetachedPlugins().stream()
+ .filter(detachedPlugin -> !detachedPlugin.getSplitWhen().isOlderThan(since))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Is the named plugin a plugin that was detached from Jenkins at some point in the past.
+ *
+ * @param pluginId The plugin ID.
+ * @return {@code true} if the plugin is a plugin that was detached from Jenkins at some
+ * point in the past, otherwise {@code false}.
+ */
+ public static boolean isDetachedPlugin(@Nonnull String pluginId) {
+ for (DetachedPlugin detachedPlugin : getDetachedPlugins()) {
+ if (detachedPlugin.getShortName().equals(pluginId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static Stream configLines(InputStream is) throws IOException {
+ return org.apache.commons.io.IOUtils.readLines(is, StandardCharsets.UTF_8).stream().filter(line -> !line.matches("#.*|\\s*"));
+ }
+
+ /**
+ * Information about plugins that were originally in the core.
+ *
+ * A detached plugin is one that has any of the following characteristics:
+ *
+ * -
+ * Was an existing plugin that at some time previously bundled with the Jenkins war file.
+ *
+ * -
+ * Was previous code in jenkins core that was split to a separate-plugin (but may not have
+ * ever been bundled in a jenkins war file - i.e. it gets split after this 2.0 update).
+ *
+ *
+ */
+ @Restricted(NoExternalUse.class)
+ public static final class DetachedPlugin {
+ private final String shortName;
+ /**
+ * Plugins built for this Jenkins version (and earlier) will automatically be assumed to have
+ * this plugin in its dependency.
+ *
+ * When core/pom.xml version is 1.123-SNAPSHOT when the code is removed, then this value should
+ * be "1.123.*" (because 1.124 will be the first version that doesn't include the removed code.)
+ */
+ private final VersionNumber splitWhen;
+ private final String requiredVersion;
+ private final VersionNumber minJavaVersion;
+
+ private DetachedPlugin(String shortName, String splitWhen, String requiredVersion, String minJavaVersion) {
+ this.shortName = shortName;
+ this.splitWhen = new VersionNumber(splitWhen);
+ this.requiredVersion = requiredVersion;
+ this.minJavaVersion = new VersionNumber(minJavaVersion);
+ }
+
+ /**
+ * Get the short name of the plugin.
+ *
+ * @return The short name of the plugin.
+ */
+ public String getShortName() {
+ return shortName;
+ }
+
+ /**
+ * Get the Jenkins version from which the plugin was detached.
+ *
+ * @return The Jenkins version from which the plugin was detached.
+ */
+ public VersionNumber getSplitWhen() {
+ return splitWhen;
+ }
+
+ /**
+ * Gets the minimum required version for the current version of Jenkins.
+ *
+ * @return the minimum required version for the current version of Jenkins.
+ * @since 2.16
+ */
+ public VersionNumber getRequiredVersion() {
+ return new VersionNumber(requiredVersion);
+ }
+
+ @Override
+ public String toString() {
+ return shortName + " " + splitWhen.toString().replace(".*", "") + " " + requiredVersion;
+ }
+
+ @Nonnull
+ public VersionNumber getMinimumJavaVersion() {
+ return minJavaVersion;
+ }
+ }
+}
diff --git a/core/src/main/resources/jenkins/split-plugin-cycles.txt b/core/src/main/resources/jenkins/split-plugin-cycles.txt
index 8c7871ef91..3cec061d7e 100644
--- a/core/src/main/resources/jenkins/split-plugin-cycles.txt
+++ b/core/src/main/resources/jenkins/split-plugin-cycles.txt
@@ -10,3 +10,25 @@ script-security command-launcher
script-security matrix-auth
script-security matrix-project
script-security windows-slaves
+
+# Weird unexpected cycle that showed up during testing of this new plugin
+# so breaking all potential cycles until JENKINS-28942
+# FIXME: remove when JENKINS-28942 is in (and used in the JAXB plugin)
+script-security jaxb
+maven-plugin jaxb
+subversion jaxb
+cvs jaxb
+ant jaxb
+javadoc jaxb
+external-monitor-job jaxb
+ldap jaxb
+pam-auth jaxb
+mailer jaxb
+matrix-auth jaxb
+windows-slaves jaxb
+antisamy-markup-formatter jaxb
+matrix-project jaxb
+junit jaxb
+bouncycastle-api jaxb
+command-launcher jaxb
+jdk-tool jaxb
diff --git a/core/src/main/resources/jenkins/split-plugins.txt b/core/src/main/resources/jenkins/split-plugins.txt
index cb0a447db2..6b09bbe877 100644
--- a/core/src/main/resources/jenkins/split-plugins.txt
+++ b/core/src/main/resources/jenkins/split-plugins.txt
@@ -1,6 +1,6 @@
# See ClassicPluginStrategy.DETACHED_LIST. As of JENKINS-47634 also used by plugin-compat-tester.
-# Columns are: plugin ID, last core release still containing the plugin's functionality, plugin version that's implied.
+# Columns are: plugin ID, last core release still containing the plugin's functionality, plugin version that's implied, minimum Java specification version where to install the plugin
# Note that all split plugins between and including matrix-auth and jdk-tool incorrectly use the first
# core release without the plugin's functionality when they should use the immediately prior release.
@@ -25,3 +25,6 @@ bouncycastle-api 2.16 2.16.0
command-launcher 2.86 1.0
# JENKINS-22367
jdk-tool 2.112 1.0
+
+# JENKINS-55681
+jaxb 2.163 2.3.0 11
diff --git a/core/src/test/java/hudson/ClassicPluginStrategyTest.java b/core/src/test/java/hudson/ClassicPluginStrategyTest.java
index bb19c5f81b..766676ad21 100644
--- a/core/src/test/java/hudson/ClassicPluginStrategyTest.java
+++ b/core/src/test/java/hudson/ClassicPluginStrategyTest.java
@@ -24,12 +24,13 @@
package hudson;
import hudson.util.VersionNumber;
+import jenkins.plugins.DetachedPluginsUtil;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
-import static hudson.ClassicPluginStrategy.DetachedPlugin;
+import static jenkins.plugins.DetachedPluginsUtil.DetachedPlugin;
/**
* @author tom.fennelly@gmail.com
@@ -38,20 +39,20 @@ public class ClassicPluginStrategyTest {
@Test
public void test_getDetachedPlugins() {
- List list = ClassicPluginStrategy.getDetachedPlugins(new VersionNumber("1.296"));
+ List list = DetachedPluginsUtil.getDetachedPlugins(new VersionNumber("1.296"));
Assert.assertTrue(list.size() >= 14); // There were 14 at the time of writing this test
Assert.assertNotNull(findPlugin("maven-plugin", list));
Assert.assertNotNull(findPlugin("subversion", list));
// Narrow the list to since "1.310" (the subversion detach version).
- list = ClassicPluginStrategy.getDetachedPlugins(new VersionNumber("1.310"));
+ list = DetachedPluginsUtil.getDetachedPlugins(new VersionNumber("1.310"));
// Maven should no longer be in the list, but subversion should.
Assert.assertNull(findPlugin("maven-plugin", list));
Assert.assertNotNull(findPlugin("subversion", list));
// Narrow the list to since "1.311" (after the subversion detach version).
- list = ClassicPluginStrategy.getDetachedPlugins(new VersionNumber("1.311"));
+ list = DetachedPluginsUtil.getDetachedPlugins(new VersionNumber("1.311"));
// Neither Maven or subversion should be in the list.
Assert.assertNull(findPlugin("maven-plugin", list));
Assert.assertNull(findPlugin("subversion", list));
diff --git a/core/src/test/java/jenkins/plugins/DetachedPluginsUtilTest.java b/core/src/test/java/jenkins/plugins/DetachedPluginsUtilTest.java
new file mode 100644
index 0000000000..611a9b34d7
--- /dev/null
+++ b/core/src/test/java/jenkins/plugins/DetachedPluginsUtilTest.java
@@ -0,0 +1,70 @@
+package jenkins.plugins;
+
+import hudson.util.VersionNumber;
+import jenkins.util.java.JavaUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class DetachedPluginsUtilTest {
+ @Test
+ public void checkJaxb() {
+ final List plugins =
+ DetachedPluginsUtil.DETACHED_LIST.stream()
+ .filter(plugin -> plugin.getShortName().equals("jaxb"))
+ .collect(Collectors.toList());
+ assertEquals(1, plugins.size());
+
+ DetachedPluginsUtil.DetachedPlugin jaxb = plugins.get(0);
+
+ assertEquals(new VersionNumber("11"), jaxb.getMinimumJavaVersion());
+
+ final List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins();
+ if (JavaUtils.isRunningWithJava8OrBelow()) {
+ assertEquals(0, detachedPlugins.stream()
+ .filter(plugin -> plugin.getShortName().equals("jaxb"))
+ .collect(Collectors.toList()).size());
+ } else if (JavaUtils.getCurrentJavaRuntimeVersionNumber().isNewerThanOrEqualTo(new VersionNumber("11.0.2"))) {
+ assertEquals(1, detachedPlugins.stream()
+ .filter(plugin -> plugin.getShortName().equals("jaxb"))
+ .collect(Collectors.toList()).size());
+
+ final List detachedPluginsSince2_161 =
+ DetachedPluginsUtil.getDetachedPlugins(new VersionNumber("2.161"));
+
+ assertEquals(1, detachedPluginsSince2_161.size());
+ assertEquals("jaxb", detachedPluginsSince2_161.get(0).getShortName());
+ }
+ }
+
+ /**
+ * Checks the format of the /jenkins/split-plugins.txt file has maximum 4 columns.
+ */
+ @Test
+ public void checkSplitPluginsFileFormat() throws IOException {
+ final List splitPluginsLines = IOUtils.readLines(getClass().getResourceAsStream("/jenkins/split-plugins.txt"));
+ assertTrue(!splitPluginsLines.isEmpty());
+
+ // File is not only comments
+ final List linesWithoutComments = splitPluginsLines.stream()
+ .filter(line -> !line.startsWith("#")).collect(Collectors.toList());
+ assertFalse( "weird, split-plugins.txt only has comments?", linesWithoutComments.isEmpty());
+
+ //
+ assertFalse("no whitespaces only lines allowed" ,linesWithoutComments.stream()
+ .filter(line -> line.trim().isEmpty())
+ .anyMatch(line -> !line.isEmpty()));
+
+
+ assertTrue( "max 4 columns is supported", linesWithoutComments.stream()
+ .map(line -> line.split(" "))
+ .noneMatch(line -> line.length > 4));
+ }
+}
diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java
index 17157ce963..2c181cdb4d 100644
--- a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java
+++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java
@@ -24,8 +24,8 @@
package jenkins.install;
-import hudson.ClassicPluginStrategy;
-import hudson.ClassicPluginStrategy.DetachedPlugin;
+import jenkins.plugins.DetachedPluginsUtil;
+import jenkins.plugins.DetachedPluginsUtil.DetachedPlugin;
import hudson.Plugin;
import hudson.PluginManager;
import hudson.PluginManagerUtil;
@@ -63,7 +63,7 @@ public class LoadDetachedPluginsTest {
public void upgradeFromJenkins1() throws IOException {
VersionNumber since = new VersionNumber("1.550");
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since);
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins(since);
assertThat("Plugins have been detached since the pre-upgrade version",
detachedPlugins.size(), greaterThan(4));
assertThat("Plugins detached between the pre-upgrade version and the current version should be installed",
@@ -78,7 +78,7 @@ public class LoadDetachedPluginsTest {
public void upgradeFromJenkins2() {
VersionNumber since = new VersionNumber("2.0");
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since);
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins(since);
assertThat("Plugins have been detached since the pre-upgrade version",
detachedPlugins.size(), greaterThan(1));
assertThat("Plugins detached between the pre-upgrade version and the current version should be installed",
@@ -93,7 +93,7 @@ public class LoadDetachedPluginsTest {
public void upgradeFromJenkins2WithNewerDependency() {
VersionNumber since = new VersionNumber("2.0");
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since);
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins(since);
assertThat("Plugins have been detached since the pre-upgrade version",
detachedPlugins.size(), greaterThan(1));
assertThat("Plugins detached between the pre-upgrade version and the current version should be installed",
@@ -111,7 +111,7 @@ public class LoadDetachedPluginsTest {
public void upgradeFromJenkins2WithOlderDependency() {
VersionNumber since = new VersionNumber("2.0");
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since);
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins(since);
assertThat("Plugins have been detached since the pre-upgrade version",
detachedPlugins.size(), greaterThan(1));
assertThat("Plugins detached between the pre-upgrade version and the current version should be installed",
@@ -131,7 +131,7 @@ public class LoadDetachedPluginsTest {
// @LocalData has command-launcher 1.2 installed, which should not be downgraded to the detached version: 1.0.
VersionNumber since = new VersionNumber("2.0");
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since);
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins(since);
assertThat("Plugins have been detached since the pre-upgrade version",
detachedPlugins.size(), greaterThan(1));
assertThat("Plugins detached between the pre-upgrade version and the current version should be installed",
@@ -146,14 +146,14 @@ public class LoadDetachedPluginsTest {
@Test
public void newInstallation() {
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins();
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins();
assertThat("Detached plugins should exist", detachedPlugins, not(empty()));
assertThat("Detached plugins should not be installed on a new instance",
getInstalledDetachedPlugins(r, detachedPlugins), empty());
assertNoFailedPlugins(r);
});
rr.then(r -> {
- List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins();
+ List detachedPlugins = DetachedPluginsUtil.getDetachedPlugins();
assertThat("Detached plugins should exist", detachedPlugins, not(empty()));
assertThat("Detached plugins should not be installed after restarting",
getInstalledDetachedPlugins(r, detachedPlugins), empty());
diff --git a/war/pom.xml b/war/pom.xml
index 73ffeb2f33..7822db814b 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -414,6 +414,12 @@ THE SOFTWARE.
1.0
hpi
+
+ io.jenkins.plugins
+ jaxb
+ 2.3.0
+ hpi
+
${project.build.directory}/${project.build.finalName}/WEB-INF/detached-plugins
true