diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 941cde4a88..dc0f25a873 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -98,6 +98,12 @@ import java.util.logging.Level; import java.util.logging.Logger; import static hudson.init.InitMilestone.*; +import hudson.util.VersionNumber; +import java.util.TreeMap; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; /** * Manages {@link PluginWrapper}s. @@ -740,7 +746,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * If the configuration (typically a job’s {@code config.xml}) * needs some plugins to be installed (or updated), those jobs * will be triggered. - * Plugins are dynamically loaded if possible (see {@link hudson.model.UpdateSite.Plugin#deploy(boolean)}). + * Plugins are dynamically loaded whenever possible. * Requires {@link Jenkins#ADMINISTER}. * @param configXml configuration that might be uploaded * @return an empty list if all is well, else a list of submitted jobs which must be completed before this configuration can be fully read @@ -748,11 +754,83 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * @see ItemGroupMixIn#createProjectFromXML * @see AbstractItem#updateByXml(javax.xml.transform.Source) * @see XStream2 + * @see hudson.model.UpdateSite.Plugin#deploy(boolean) + * @see PluginWrapper#supportsDynamicLoad + * @see hudson.model.UpdateCenter.DownloadJob.SuccessButRequiresRestart * @since XXX */ public List> prevalidateConfig(InputStream configXml) throws IOException { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); - return Collections.emptyList(); // XXX + List> jobs = new ArrayList>(); + UpdateCenter uc = Jenkins.getInstance().getUpdateCenter(); + // XXX call uc.updateAllSites() when available? perhaps not, since we should not block on network here + for (Map.Entry requestedPlugin : parseRequestedPlugins(configXml).entrySet()) { + PluginWrapper pw = getPlugin(requestedPlugin.getKey()); + if (pw == null) { // install new + UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey()); + if (toInstall == null) { + LOGGER.log(Level.WARNING, "No such plugin {0} to install", requestedPlugin.getKey()); + continue; + } + if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) { + LOGGER.log(Level.WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version}); + } + if (toInstall.isForNewerHudson()) { + LOGGER.log(Level.WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version}); + } + jobs.add(toInstall.deploy(true)); + } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade + UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey()); + if (toInstall == null) { + LOGGER.log(Level.WARNING, "No such plugin {0} to upgrade", requestedPlugin.getKey()); + continue; + } + if (!pw.isOlderThan(new VersionNumber(toInstall.version))) { + LOGGER.log(Level.WARNING, "{0}@{1} is no newer than what we already have", new Object[] {toInstall.name, toInstall.version}); + continue; + } + if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) { + LOGGER.log(Level.WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version}); + } + if (toInstall.isForNewerHudson()) { + LOGGER.log(Level.WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version}); + } + if (!toInstall.isCompatibleWithInstalledVersion()) { + LOGGER.log(Level.WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()}); + } + jobs.add(toInstall.deploy(true)); // dynamicLoad=true => sure to throw RestartRequiredException, but at least message is nicer + } // else already good + } + return jobs; + } + + static Map parseRequestedPlugins(InputStream configXml) throws IOException { + final Map requestedPlugins = new TreeMap(); + try { + SAXParserFactory.newInstance().newSAXParser().parse(configXml, new DefaultHandler() { + @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + String plugin = attributes.getValue("plugin"); + if (plugin == null) { + return; + } + if (!plugin.matches("[^@]+@[^@]+")) { + throw new SAXException("Malformed plugin attribute: " + plugin); + } + int at = plugin.indexOf('@'); + String shortName = plugin.substring(0, at); + VersionNumber existing = requestedPlugins.get(shortName); + VersionNumber requested = new VersionNumber(plugin.substring(at + 1)); + if (existing == null || existing.compareTo(requested) < 0) { + requestedPlugins.put(shortName, requested); + } + } + }); + } catch (IOException x) { + throw x; + } catch (Exception x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + return requestedPlugins; } /** diff --git a/core/src/test/java/hudson/PluginManagerTest.java b/core/src/test/java/hudson/PluginManagerTest.java new file mode 100644 index 0000000000..73ed836a95 --- /dev/null +++ b/core/src/test/java/hudson/PluginManagerTest.java @@ -0,0 +1,37 @@ +/* + * The MIT License + * + * Copyright 2012 Jesse Glick. + * + * 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; + +import org.apache.tools.ant.filters.StringInputStream; +import org.junit.Test; +import static org.junit.Assert.*; + +public class PluginManagerTest { + + @Test public void parseRequestedPlugins() throws Exception { + assertEquals("{other=2.0, stuff=1.2}", PluginManager.parseRequestedPlugins(new StringInputStream("")).toString()); + } + +} diff --git a/test/src/test/java/hudson/PluginManagerTest.java b/test/src/test/java/hudson/PluginManagerTest.java index b00307d565..eef54e7248 100644 --- a/test/src/test/java/hudson/PluginManagerTest.java +++ b/test/src/test/java/hudson/PluginManagerTest.java @@ -231,6 +231,8 @@ public class PluginManagerTest extends HudsonTestCase { UpdateCenter.InstallationJob ijob = (UpdateCenter.InstallationJob) job; assertEquals("tasks", ijob.plugin.name); assertNotNull(jenkins.getPluginManager().getPlugin("tasks")); + // XXX restart scheduled (SuccessButRequiresRestart) after upgrade or Support-Dynamic-Loading: false + // XXX dependencies installed or upgraded too } }