org.jenkins.tools.test.PluginCompatTester.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkins.tools.test.PluginCompatTester.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
 * Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe,
 * Stephen Connolly, Tom Huybrechts, Yahoo! Inc., Alan Harder, 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 org.jenkins.tools.test;

import hudson.Functions;
import hudson.maven.MavenEmbedderException;
import hudson.model.UpdateSite;
import hudson.model.UpdateSite.Plugin;
import hudson.util.VersionNumber;
import java.io.BufferedReader;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmTag;
import org.apache.maven.scm.command.checkout.CheckOutScmResult;
import org.apache.maven.scm.manager.ScmManager;
import org.apache.maven.scm.repository.ScmRepository;
import org.codehaus.plexus.PlexusContainerException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.io.RawInputStreamFacade;
import org.jenkins.tools.test.exception.PluginSourcesUnavailableException;
import org.jenkins.tools.test.exception.PomExecutionException;
import org.jenkins.tools.test.exception.PomTransformationException;
import org.jenkins.tools.test.model.MavenCoordinates;
import org.jenkins.tools.test.model.MavenPom;
import org.jenkins.tools.test.model.PluginCompatReport;
import org.jenkins.tools.test.model.PluginCompatResult;
import org.jenkins.tools.test.model.PluginCompatTesterConfig;
import org.jenkins.tools.test.model.PluginInfos;
import org.jenkins.tools.test.model.PluginRemoting;
import org.jenkins.tools.test.model.PomData;
import org.jenkins.tools.test.model.TestExecutionResult;
import org.jenkins.tools.test.model.TestStatus;
import org.springframework.core.io.ClassPathResource;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jenkins.tools.test.maven.ExternalMavenRunner;
import org.jenkins.tools.test.maven.InternalMavenRunner;
import org.jenkins.tools.test.maven.MavenRunner;

/**
 * Frontend for plugin compatibility tests
 * @author Frederic Camblor, Olivier Lamy
 */
public class PluginCompatTester {

    private static final String DEFAULT_SOURCE_ID = "default";

    private PluginCompatTesterConfig config;
    private final MavenRunner runner;

    public PluginCompatTester(PluginCompatTesterConfig config) {
        this.config = config;
        runner = config.getExternalMaven() == null ? new InternalMavenRunner()
                : new ExternalMavenRunner(config.getExternalMaven());
    }

    private SortedSet<MavenCoordinates> generateCoreCoordinatesToTest(UpdateSite.Data data,
            PluginCompatReport previousReport) {
        SortedSet<MavenCoordinates> coreCoordinatesToTest = null;
        // If parent GroupId/Artifact are not null, this will be fast : we will only test
        // against 1 core coordinate
        if (config.getParentGroupId() != null && config.getParentArtifactId() != null) {
            coreCoordinatesToTest = new TreeSet<MavenCoordinates>();

            // If coreVersion is not provided in PluginCompatTesterConfig, let's use latest core
            // version used in update center
            String coreVersion = config.getParentVersion() == null ? data.core.version : config.getParentVersion();

            MavenCoordinates coreArtifact = new MavenCoordinates(config.getParentGroupId(),
                    config.getParentArtifactId(), coreVersion);
            coreCoordinatesToTest.add(coreArtifact);
            // If parent groupId/artifactId are null, we'll test against every already recorded
            // cores
        } else if (config.getParentGroupId() == null && config.getParentArtifactId() == null) {
            coreCoordinatesToTest = previousReport.getTestedCoreCoordinates();
        } else {
            throw new IllegalStateException(
                    "config.parentGroupId and config.parentArtifactId should either be both null or both filled\n"
                            + "config.parentGroupId=" + String.valueOf(config.getParentGroupId())
                            + ", config.parentArtifactId=" + String.valueOf(config.getParentArtifactId()));
        }

        return coreCoordinatesToTest;
    }

    public PluginCompatReport testPlugins() throws PlexusContainerException, IOException, MavenEmbedderException {
        // Providing XSL Stylesheet along xml report file
        if (config.reportFile != null) {
            if (config.isProvideXslReport()) {
                File xslFilePath = PluginCompatReport.getXslFilepath(config.reportFile);
                FileUtils.copyStreamToFile(new RawInputStreamFacade(getXslTransformerResource().getInputStream()),
                        xslFilePath);
            }
        }

        DataImporter dataImporter = null;
        if (config.getGaeBaseUrl() != null && config.getGaeSecurityToken() != null) {
            dataImporter = new DataImporter(config.getGaeBaseUrl(), config.getGaeSecurityToken());
        }

        HashMap<String, String> pluginGroupIds = new HashMap<String, String>(); // Used to track real plugin groupIds from WARs
        UpdateSite.Data data = config.getWar() == null ? extractUpdateCenterData()
                : scanWAR(config.getWar(), pluginGroupIds);
        PluginCompatReport report = PluginCompatReport.fromXml(config.reportFile);

        SortedSet<MavenCoordinates> testedCores = config.getWar() == null
                ? generateCoreCoordinatesToTest(data, report)
                : coreVersionFromWAR(data);

        MavenRunner.Config mconfig = new MavenRunner.Config();
        mconfig.userSettingsFile = config.getM2SettingsFile();
        // TODO REMOVE
        mconfig.userProperties.put("failIfNoTests", "false");
        mconfig.userProperties.put("argLine", "-XX:MaxPermSize=128m");
        String mavenPropertiesFilePath = this.config.getMavenPropertiesFile();
        if (StringUtils.isNotBlank(mavenPropertiesFilePath)) {
            File file = new File(mavenPropertiesFilePath);
            if (file.exists()) {
                FileInputStream fileInputStream = null;
                try {
                    fileInputStream = new FileInputStream(file);
                    Properties properties = new Properties();
                    properties.load(fileInputStream);
                    for (Map.Entry<Object, Object> entry : properties.entrySet()) {
                        mconfig.userProperties.put((String) entry.getKey(), (String) entry.getValue());
                    }
                } finally {
                    IOUtils.closeQuietly(fileInputStream);
                }
            } else {
                System.out.println("File " + mavenPropertiesFilePath + " not exists");
            }
        }

        SCMManagerFactory.getInstance().start();
        for (MavenCoordinates coreCoordinates : testedCores) {
            System.out.println("Starting plugin tests on core coordinates : " + coreCoordinates.toString());
            for (Plugin plugin : data.plugins.values()) {
                if (config.getIncludePlugins() == null
                        || config.getIncludePlugins().contains(plugin.name.toLowerCase())) {
                    PluginInfos pluginInfos = new PluginInfos(plugin.name, plugin.version, plugin.url);

                    if (config.getExcludePlugins() != null
                            && config.getExcludePlugins().contains(plugin.name.toLowerCase())) {
                        System.out.println("Plugin " + plugin.name + " is in excluded plugins => test skipped !");
                        continue;
                    }

                    String errorMessage = null;
                    TestStatus status = null;

                    MavenCoordinates actualCoreCoordinates = coreCoordinates;
                    PluginRemoting remote = new PluginRemoting(plugin.url);
                    PomData pomData;
                    try {
                        pomData = remote.retrievePomData();
                        System.out.println("detected parent POM " + pomData.parent.toGAV());
                        if ((pomData.parent.groupId.equals(PluginCompatTesterConfig.DEFAULT_PARENT_GROUP)
                                && pomData.parent.artifactId
                                        .equals(PluginCompatTesterConfig.DEFAULT_PARENT_ARTIFACT)
                                || pomData.parent.groupId.equals("org.jvnet.hudson.plugins"))
                                && coreCoordinates.version.matches("1[.][0-9]+[.][0-9]+")
                                && new VersionNumber(coreCoordinates.version)
                                        .compareTo(new VersionNumber("1.485")) < 0) { // TODO unless 1.480.3+
                            System.out.println("Cannot test against " + coreCoordinates.version
                                    + " due to lack of deployed POM for " + coreCoordinates.toGAV());
                            actualCoreCoordinates = new MavenCoordinates(coreCoordinates.groupId,
                                    coreCoordinates.artifactId,
                                    coreCoordinates.version.replaceFirst("[.][0-9]+$", ""));
                        }
                    } catch (Throwable t) {
                        status = TestStatus.INTERNAL_ERROR;
                        errorMessage = t.getMessage();
                        pomData = null;
                    }

                    if (!config.isSkipTestCache() && report.isCompatTestResultAlreadyInCache(pluginInfos,
                            actualCoreCoordinates, config.getTestCacheTimeout(), config.getCacheThresholStatus())) {
                        System.out.println(
                                "Cache activated for plugin " + pluginInfos.pluginName + " => test skipped !");
                        continue; // Don't do anything : we are in the cached interval ! :-)
                    }

                    List<String> warningMessages = new ArrayList<String>();
                    if (errorMessage == null) {
                        try {
                            TestExecutionResult result = testPluginAgainst(actualCoreCoordinates, plugin, mconfig,
                                    pomData, data.plugins, pluginGroupIds);
                            // If no PomExecutionException, everything went well...
                            status = TestStatus.SUCCESS;
                            warningMessages.addAll(result.pomWarningMessages);
                        } catch (PomExecutionException e) {
                            if (!e.succeededPluginArtifactIds.contains("maven-compiler-plugin")) {
                                status = TestStatus.COMPILATION_ERROR;
                            } else if (!e.succeededPluginArtifactIds.contains("maven-surefire-plugin")) {
                                status = TestStatus.TEST_FAILURES;
                            } else { // Can this really happen ???
                                status = TestStatus.SUCCESS;
                            }
                            errorMessage = e.getErrorMessage();
                            warningMessages.addAll(e.getPomWarningMessages());
                        } catch (Error e) {
                            // Rethrow the error ... something is getting wrong !
                            throw e;
                        } catch (Throwable t) {
                            status = TestStatus.INTERNAL_ERROR;
                            errorMessage = t.getMessage();
                        }
                    }

                    File buildLogFile = createBuildLogFile(config.reportFile, plugin.name, plugin.version,
                            actualCoreCoordinates);
                    String buildLogFilePath = "";
                    if (buildLogFile.exists()) {
                        buildLogFilePath = createBuildLogFilePathFor(pluginInfos.pluginName,
                                pluginInfos.pluginVersion, actualCoreCoordinates);
                    }
                    PluginCompatResult result = new PluginCompatResult(actualCoreCoordinates, status, errorMessage,
                            warningMessages, buildLogFilePath);
                    report.add(pluginInfos, result);

                    // Adding result to GAE
                    if (dataImporter != null) {
                        dataImporter.importPluginCompatResult(result, pluginInfos,
                                config.reportFile.getParentFile());
                        // TODO: import log files
                    }

                    if (config.reportFile != null) {
                        if (!config.reportFile.exists()) {
                            FileUtils.fileWrite(config.reportFile.getAbsolutePath(), "");
                        }
                        report.save(config.reportFile);
                    }
                } else {
                    System.out.println("Plugin " + plugin.name + " not in included plugins => test skipped !");
                }
            }
        }

        // Generating HTML report if needed
        if (config.reportFile != null) {
            if (config.isGenerateHtmlReport()) {
                generateHtmlReportFile();
            }
        }

        return report;
    }

    private void generateHtmlReportFile() throws IOException {
        Source xmlSource = new StreamSource(config.reportFile);
        Source xsltSource = new StreamSource(getXslTransformerResource().getInputStream());
        Result result = new StreamResult(PluginCompatReport.getHtmlFilepath(config.reportFile));

        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = null;
        try {
            transformer = factory.newTransformer(xsltSource);
            transformer.transform(xmlSource, result);
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
    }

    private static ClassPathResource getXslTransformerResource() {
        return new ClassPathResource("resultToReport.xsl");
    }

    private static File createBuildLogFile(File reportFile, String pluginName, String pluginVersion,
            MavenCoordinates coreCoords) {
        return new File(reportFile.getParentFile().getAbsolutePath() + "/"
                + createBuildLogFilePathFor(pluginName, pluginVersion, coreCoords));
    }

    private static String createBuildLogFilePathFor(String pluginName, String pluginVersion,
            MavenCoordinates coreCoords) {
        return String.format("logs/%s/v%s_against_%s_%s_%s.log", pluginName, pluginVersion, coreCoords.groupId,
                coreCoords.artifactId, coreCoords.version);
    }

    private TestExecutionResult testPluginAgainst(MavenCoordinates coreCoordinates, Plugin plugin,
            MavenRunner.Config mconfig, PomData pomData, Map<String, Plugin> otherPlugins,
            Map<String, String> pluginGroupIds) throws PluginSourcesUnavailableException,
            PomTransformationException, PomExecutionException, IOException {
        System.out.println(String.format("%n%n%n%n%n"));
        System.out.println(String.format("#############################################"));
        System.out.println(String.format("#############################################"));
        System.out.println(String.format("##%n## Starting to test plugin %s v%s%n## against %s%n##", plugin.name,
                plugin.version, coreCoordinates));
        System.out.println(String.format("#############################################"));
        System.out.println(String.format("#############################################"));
        System.out.println(String.format("%n%n%n%n%n"));

        File pluginCheckoutDir = new File(config.workDirectory.getAbsolutePath() + "/" + plugin.name + "/");
        if (pluginCheckoutDir.exists()) {
            System.out.println("Deleting working directory " + pluginCheckoutDir.getAbsolutePath());
            FileUtils.deleteDirectory(pluginCheckoutDir);
        }
        pluginCheckoutDir.mkdir();
        System.out.println("Created plugin checkout dir : " + pluginCheckoutDir.getAbsolutePath());

        try {
            System.out.println("Checking out from SCM connection URL : " + pomData.getConnectionUrl() + " ("
                    + plugin.name + "-" + plugin.version + ")");
            ScmManager scmManager = SCMManagerFactory.getInstance().createScmManager();
            ScmRepository repository = scmManager.makeScmRepository(pomData.getConnectionUrl());
            CheckOutScmResult result = scmManager.checkOut(repository, new ScmFileSet(pluginCheckoutDir),
                    new ScmTag(plugin.name + "-" + plugin.version));

            if (!result.isSuccess()) {
                if (result.getCommandOutput().contains("error: pathspec")
                        && result.getCommandOutput().contains("did not match any file(s) known to git.")) {
                    // Trying to look for existing branch that looks like the one we are looking for
                    // TODO ???
                } else {
                    throw new RuntimeException(result.getProviderMessage() + "||" + result.getCommandOutput());
                }
            }
        } catch (ComponentLookupException e) {
            System.err.println("Error : " + e.getMessage());
            throw new PluginSourcesUnavailableException("Problem while creating ScmManager !", e);
        } catch (Exception e) {
            System.err.println("Error : " + e.getMessage());
            throw new PluginSourcesUnavailableException("Problem while checking out plugin sources!", e);
        }

        List<String> args = new ArrayList<String>();
        boolean mustTransformPom = false;
        // TODO future versions of DEFAULT_PARENT_GROUP/ARTIFACT may be able to use this as well
        if (pomData.parent.groupId.equals("com.cloudbees.jenkins.plugins")
                && pomData.parent.artifactId.equals("jenkins-plugins") ||
        // TODO ought to analyze the chain of parent POMs, which would lead to com.cloudbees.jenkins.plugins:jenkins-plugins in this case:
                pomData.parent.groupId.equals("com.cloudbees.operations-center.common")
                        && pomData.parent.artifactId.equals("operations-center-parent")
                || pomData.parent.groupId.equals("com.cloudbees.operations-center.client")
                        && pomData.parent.artifactId.equals("operations-center-parent-client")) {
            args.add("-Djenkins.version=" + coreCoordinates.version);
            args.add("-Dhpi-plugin.version=1.99"); // TODO would ideally pick up exact version from org.jenkins-ci.main:pom
        } else {
            mustTransformPom = true;
        }

        File buildLogFile = createBuildLogFile(config.reportFile, plugin.name, plugin.version, coreCoordinates);
        FileUtils.forceMkdir(buildLogFile.getParentFile()); // Creating log directory
        FileUtils.fileWrite(buildLogFile.getAbsolutePath(), ""); // Creating log file

        boolean ranCompile = false;
        try {
            // First build against the original POM.
            // This defends against source incompatibilities (which we do not care about for this purpose);
            // and ensures that we are testing a plugin binary as close as possible to what was actually released.
            runner.run(mconfig, pluginCheckoutDir, buildLogFile, "clean", "process-test-classes");
            ranCompile = true;

            // Then transform the POM and run tests against that.
            // You might think that it would suffice to run e.g.
            // -Dmaven-surefire-plugin.version=2.15 -Dmaven.test.dependency.excludes=org.jenkins-ci.main:jenkins-war -Dmaven.test.additionalClasspath=//org/jenkins-ci/main/jenkins-war/1.580.1/jenkins-war-1.580.1.war clean test
            // (2.15+ required for ${maven.test.dependency.excludes} and ${maven.test.additionalClasspath} to be honored from CLI)
            // but it does not work; there are lots of linkage errors as some things are expected to be in the test classpath which are not.
            // Much simpler to do use the parent POM to set up the test classpath.
            MavenPom pom = new MavenPom(pluginCheckoutDir);
            try {
                addSplitPluginDependencies(plugin.name, mconfig, pluginCheckoutDir, pom, otherPlugins,
                        pluginGroupIds);
            } catch (Exception x) {
                x.printStackTrace();
                pomData.getWarningMessages().add(Functions.printThrowable(x));
                // but continue
            }
            if (mustTransformPom) {
                pom.transformPom(coreCoordinates);
            }
            args.add("--define=maven.test.redirectTestOutputToFile=false");
            args.add("--define=concurrency=1");
            args.add("hpi:resolve-test-dependencies");
            args.add("hpi:test-hpl");
            args.add("surefire:test");
            runner.run(mconfig, pluginCheckoutDir, buildLogFile, args.toArray(new String[args.size()]));

            return new TestExecutionResult(pomData.getWarningMessages());
        } catch (PomExecutionException e) {
            PomExecutionException e2 = new PomExecutionException(e);
            e2.getPomWarningMessages().addAll(pomData.getWarningMessages());
            if (ranCompile) {
                // So the status is considered to be TEST_FAILURES not COMPILATION_ERROR:
                e2.succeededPluginArtifactIds.add("maven-compiler-plugin");
            }
            throw e2;
        }
    }

    private UpdateSite.Data extractUpdateCenterData() {
        URL url = null;
        String jsonp = null;
        try {
            url = new URL(config.updateCenterUrl);
            jsonp = IOUtils.toString(url.openStream());
        } catch (IOException e) {
            throw new RuntimeException("Invalid update center url : " + config.updateCenterUrl, e);
        }

        String json = jsonp.substring(jsonp.indexOf('(') + 1, jsonp.lastIndexOf(')'));
        UpdateSite us = new UpdateSite(DEFAULT_SOURCE_ID, url.toExternalForm());
        return newUpdateSiteData(us, JSONObject.fromObject(json));
    }

    /**
     * Scans through a WAR file, accumulating plugin information
     * @param war WAR to scan
     * @param pluginGroupIds Map pluginName to groupId if set in the manifest, MUTATED IN THE EXECUTION
     * @return Update center data
     * @throws IOException
     */
    private UpdateSite.Data scanWAR(File war, Map<String, String> pluginGroupIds) throws IOException {
        JSONObject top = new JSONObject();
        top.put("id", DEFAULT_SOURCE_ID);
        JSONObject plugins = new JSONObject();
        JarFile jf = new JarFile(war);
        if (pluginGroupIds == null) {
            pluginGroupIds = new HashMap<String, String>();
        }
        try {
            Enumeration<JarEntry> entries = jf.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                Matcher m = Pattern.compile("WEB-INF/lib/jenkins-core-([0-9.]+(?:-[0-9.]+)?(?:-SNAPSHOT)?)[.]jar")
                        .matcher(name);
                if (m.matches()) {
                    if (top.has("core")) {
                        throw new IOException(">1 jenkins-core.jar in " + war);
                    }
                    top.put("core", new JSONObject().accumulate("name", "core").accumulate("version", m.group(1))
                            .accumulate("url", ""));
                }
                m = Pattern.compile("WEB-INF/(?:optional-)?plugins/([^/.]+)[.][hj]pi").matcher(name);
                if (m.matches()) {
                    JSONObject plugin = new JSONObject().accumulate("url", "");
                    InputStream is = jf.getInputStream(entry);
                    try {
                        JarInputStream jis = new JarInputStream(is);
                        try {
                            Manifest manifest = jis.getManifest();
                            String shortName = manifest.getMainAttributes().getValue("Short-Name");
                            if (shortName == null) {
                                shortName = manifest.getMainAttributes().getValue("Extension-Name");
                                if (shortName == null) {
                                    shortName = m.group(1);
                                }
                            }
                            plugin.put("name", shortName);
                            pluginGroupIds.put(shortName, manifest.getMainAttributes().getValue("Group-Id"));
                            plugin.put("version", manifest.getMainAttributes().getValue("Plugin-Version"));
                            plugin.put("url", "jar:" + war.toURI() + "!/" + name);
                            JSONArray dependenciesA = new JSONArray();
                            String dependencies = manifest.getMainAttributes().getValue("Plugin-Dependencies");
                            if (dependencies != null) {
                                // e.g. matrix-auth:1.0.2;resolution:=optional,credentials:1.8.3;resolution:=optional
                                for (String pair : dependencies.replace(";resolution:=optional", "").split(",")) {
                                    String[] nameVer = pair.split(":");
                                    assert nameVer.length == 2;
                                    dependenciesA.add(new JSONObject().accumulate("name", nameVer[0])
                                            .accumulate("version", nameVer[1])
                                            ./* we do care about even optional deps here */accumulate("optional",
                                                    "false"));
                                }
                            }
                            plugin.accumulate("dependencies", dependenciesA);
                            plugins.put(shortName, plugin);
                        } finally {
                            jis.close();
                        }
                    } finally {
                        is.close();
                    }
                }
            }
        } finally {
            jf.close();
        }
        top.put("plugins", plugins);
        if (!top.has("core")) {
            throw new IOException("no jenkins-core.jar in " + war);
        }
        System.out.println("Scanned contents of " + war + ": " + top);
        return newUpdateSiteData(new UpdateSite(DEFAULT_SOURCE_ID, null), top);
    }

    private SortedSet<MavenCoordinates> coreVersionFromWAR(UpdateSite.Data data) {
        SortedSet<MavenCoordinates> result = new TreeSet<MavenCoordinates>();
        result.add(new MavenCoordinates(PluginCompatTesterConfig.DEFAULT_PARENT_GROUP,
                PluginCompatTesterConfig.DEFAULT_PARENT_ARTIFACT, data.core.version));
        return result;
    }

    private UpdateSite.Data newUpdateSiteData(UpdateSite us, JSONObject jsonO) throws RuntimeException {
        try {
            Constructor<UpdateSite.Data> dataConstructor = UpdateSite.Data.class
                    .getDeclaredConstructor(UpdateSite.class, JSONObject.class);
            dataConstructor.setAccessible(true);
            return dataConstructor.newInstance(us, jsonO);
        } catch (Exception e) {
            throw new RuntimeException("UpdateSite.Data instanciation problems", e);
        }
    }

    private void addSplitPluginDependencies(String thisPlugin, MavenRunner.Config mconfig, File pluginCheckoutDir,
            MavenPom pom, Map<String, Plugin> otherPlugins, Map<String, String> pluginGroupIds)
            throws PomExecutionException, IOException {
        File tmp = File.createTempFile("dependencies", ".log");
        VersionNumber coreDep = null;
        Map<String, VersionNumber> pluginDeps = new HashMap<String, VersionNumber>();
        try {
            runner.run(mconfig, pluginCheckoutDir, tmp, "dependency:resolve");
            Reader r = new FileReader(tmp);
            try {
                BufferedReader br = new BufferedReader(r);
                // TODO could include |test but only if pom.addDependencies would add as <scope>test</scope>
                Pattern p = Pattern.compile(
                        "\\[INFO\\]    ([^:]+):([^:]+):([a-z-]+):([^:]+):(provided|compile|runtime|system)");
                String line;
                while ((line = br.readLine()) != null) {
                    Matcher m = p.matcher(line);
                    if (!m.matches()) {
                        continue;
                    }
                    String groupId = m.group(1);
                    String artifactId = m.group(2);
                    VersionNumber version;
                    try {
                        version = new VersionNumber(m.group(4));
                    } catch (IllegalArgumentException x) {
                        // OK, some other kind of dep, just ignore
                        continue;
                    }
                    if (groupId.equals("org.jenkins-ci.main") && artifactId.equals("jenkins-core")) {
                        coreDep = version;
                    } else if (groupId.equals("org.jenkins-ci.plugins")) {
                        pluginDeps.put(artifactId, version);
                    } else if (groupId.equals("org.jenkins-ci.main") && artifactId.equals("maven-plugin")) {
                        pluginDeps.put(artifactId, version);
                    } else if (groupId.equals(pluginGroupIds.get(artifactId))) {
                        pluginDeps.put(artifactId, version);
                    }
                }
            } finally {
                r.close();
            }
        } finally {
            tmp.delete();
        }
        System.out.println("Analysis: coreDep=" + coreDep + " pluginDeps=" + pluginDeps);
        if (coreDep != null) {
            // Synchronize with ClassicPluginStrategy.DETACHED_LIST:
            String[] splits = { "maven-plugin:1.296:1.296", "subversion:1.310:1.0", "cvs:1.340:0.1",
                    "ant:1.430.*:1.0", "javadoc:1.430.*:1.0", "external-monitor-job:1.467.*:1.0",
                    "ldap:1.467.*:1.0", "pam-auth:1.467.*:1.0", "mailer:1.493.*:1.2", "matrix-auth:1.535.*:1.0.2",
                    "windows-slaves:1.547.*:1.0", "antisamy-markup-formatter:1.553.*:1.0",
                    "matrix-project:1.561.*:1.0", "junit:1.577.*:1.0", };
            // Synchronize with ClassicPluginStrategy.BREAK_CYCLES:
            String[] exceptions = { "script-security/matrix-auth", "script-security/windows-slaves",
                    "script-security/antisamy-markup-formatter", "script-security/matrix-project",
                    "credentials/matrix-auth", "credentials/windows-slaves" };
            Map<String, VersionNumber> toAdd = new HashMap<String, VersionNumber>();
            Map<String, VersionNumber> toReplace = new HashMap<String, VersionNumber>();
            for (String split : splits) {
                String[] pieces = split.split(":");
                String plugin = pieces[0];
                if (Arrays.asList(exceptions).contains(thisPlugin + "/" + plugin)) {
                    System.out.println("Skipping implicit dep " + thisPlugin + "  " + plugin);
                    continue;
                }
                VersionNumber splitPoint = new VersionNumber(pieces[1]);
                VersionNumber declaredMinimum = new VersionNumber(pieces[2]);
                // TODO this should only happen if the tested core version is  splitPoint
                // TODO pluginDeps could include test deps inherited from jenkins-test-harness; do we need to add/replace these implicit deps?
                if (coreDep.compareTo(splitPoint) <= 0 && !pluginDeps.containsKey(plugin)) {
                    Plugin bundledP = otherPlugins.get(plugin);
                    if (bundledP != null) {
                        VersionNumber bundledV;
                        try {
                            bundledV = new VersionNumber(bundledP.version);
                        } catch (NumberFormatException x) { // TODO apparently this does not handle `1.0-beta-1` and the like?!
                            System.out.println(
                                    "Skipping unparseable dep on " + bundledP.name + ": " + bundledP.version);
                            continue;
                        }
                        if (bundledV.isNewerThan(declaredMinimum)) {
                            toAdd.put(plugin, bundledV);
                            continue;
                        }
                    }
                    toAdd.put(plugin, declaredMinimum);
                }
            }
            for (Map.Entry<String, VersionNumber> pluginDep : pluginDeps.entrySet()) {
                String plugin = pluginDep.getKey();
                Plugin bundledP = otherPlugins.get(plugin);
                if (bundledP != null) {
                    VersionNumber bundledV = new VersionNumber(bundledP.version);
                    if (bundledV.isNewerThan(pluginDep.getValue())) {
                        assert !toAdd.containsKey(plugin);
                        toReplace.put(plugin, bundledV);
                    }
                    // Also check any dependencies, so if we are upgrading cloudbees-folder, we also add an explicit dep on a bundled credentials.
                    for (Map.Entry<String, String> dependency : bundledP.dependencies.entrySet()) {
                        String depPlugin = dependency.getKey();
                        if (pluginDeps.containsKey(depPlugin)) {
                            continue; // already handled
                        }
                        // We ignore the declared dependency version and go with the bundled version:
                        Plugin depBundledP = otherPlugins.get(depPlugin);
                        if (depBundledP != null) {
                            System.out.println("Adding " + depPlugin + " since it was a dependency of " + plugin);
                            toAdd.put(depPlugin, new VersionNumber(depBundledP.version));
                        }
                    }
                }
            }
            if (!toAdd.isEmpty() || !toReplace.isEmpty()) {
                System.out.println(
                        "Adding/replacing plugin dependencies for compatibility: " + toAdd + " " + toReplace);
                pom.addDependencies(toAdd, toReplace, coreDep, pluginGroupIds);
            }
        }
    }

}