/* * The MIT License * * Copyright (c) 2016, Matthew DeTullio * * 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 com.github.mjdetullio.jenkins.plugins.multibranch; import hudson.BulkChange; import hudson.XmlFile; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Item; import hudson.model.Items; import hudson.model.Saveable; import hudson.model.TopLevelItem; import hudson.util.AtomicFileWriter; import jenkins.branch.Branch; import jenkins.branch.BranchProjectFactory; import jenkins.branch.BranchProperty; import jenkins.model.Jenkins; import jenkins.scm.api.SCMHead; import jenkins.security.NotReallyRoleSensitiveCallable; import jenkins.util.xml.XMLUtils; import org.xml.sax.SAXException; import javax.annotation.Nonnull; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.IOException; import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; /** * @author Matthew DeTullio */ public abstract class TemplateDrivenBranchProjectFactory<P extends AbstractProject<P, B> & TopLevelItem, B extends AbstractBuild<P, B>> extends BranchProjectFactory<P, B> { private static final String CLASSNAME = TemplateDrivenBranchProjectFactory.class.getName(); private static final Logger LOGGER = Logger.getLogger(CLASSNAME); @Nonnull @Override public Branch getBranch(@Nonnull P project) { BranchProjectProperty property = project.getProperty(BranchProjectProperty.class); /* * Ugly hackish stuff, in the event that the user configures a branch project directly, thereby removing the * BranchProjectProperty. The property must exist and we can't bash the @Nonnull return value restriction! * * Fudge some generic Branch with the expectation that indexing will soon reset the Branch with proper values, * or that it will be converted to Branch.Dead and the guessed values for sourceId and properties won't matter. */ if (property == null) { Branch branch = new Branch("unknown", new SCMHead(project.getDisplayName()), project.getScm(), Collections.<BranchProperty>emptyList()); setBranch(project, branch); return branch; } return property.getBranch(); } @Nonnull @Override public P setBranch(@Nonnull P project, @Nonnull Branch branch) { BranchProjectProperty property = project.getProperty(BranchProjectProperty.class); BulkChange bc = new BulkChange(project); try { if (property == null) { project.addProperty(new BranchProjectProperty<P, B>(branch)); } else { property.setBranch(branch); } project.setScm(branch.getScm()); bc.commit(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to set BranchProjectProperty", e); bc.abort(); } return project; } /** * Decorates projects by using {@link #updateByXml(AbstractProject, Source)} and saving the configuration, * rather than only updating the project in memory. * * @param project the project to decorate * @return the project that was just decorated */ @Override public P decorate(P project) { if (!isProject(project)) { return project; } if (!(getOwner() instanceof TemplateDrivenMultiBranchProject)) { throw new IllegalStateException(String.format("%s can only be used with %s.", TemplateDrivenBranchProjectFactory.class.getSimpleName(), TemplateDrivenMultiBranchProject.class.getSimpleName())); } TemplateDrivenMultiBranchProject<P, B> owner = (TemplateDrivenMultiBranchProject<P, B>) getOwner(); Branch branch = getBranch(project); String displayName = project.getDisplayNameOrNull(); boolean wasDisabled = project.isDisabled(); BulkChange bc = new BulkChange(project); try { updateByXml(project, new StreamSource(owner.getTemplate().getConfigFile().readRaw())); // Restore settings managed by this plugin setBranch(project, branch); project.setDisplayName(displayName); project.setScm(branch.getScm()); // Workarounds for JENKINS-21017 project.setBuildDiscarder(owner.getTemplate().getBuildDiscarder()); project.setCustomWorkspace(owner.getTemplate().getCustomWorkspace()); if (!wasDisabled) { project.enable(); } project = super.decorate(project); bc.commit(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to update project " + project.getName(), e); } finally { bc.abort(); } return project; } /** * This is a mirror of {@link hudson.model.AbstractItem#updateByXml(Source)} without the * {@link hudson.model.listeners.SaveableListener#fireOnChange(Saveable, XmlFile)} trigger. * * @param project project to update by XML * @param source source of XML * @throws IOException if error performing update */ @SuppressWarnings("ThrowFromFinallyBlock") private void updateByXml(final P project, Source source) throws IOException { project.checkPermission(Item.CONFIGURE); final String projectName = project.getName(); XmlFile configXmlFile = project.getConfigFile(); final AtomicFileWriter out = new AtomicFileWriter(configXmlFile.getFile()); try { try { XMLUtils.safeTransform(source, new StreamResult(out)); out.close(); } catch (SAXException | TransformerException e) { throw new IOException("Failed to persist config.xml", e); } // try to reflect the changes by reloading Object o = new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(project); if (o != project) { // ensure that we've got the same job type. extending this code to support updating // to different job type requires destroying & creating a new job type throw new IOException("Expecting " + project.getClass() + " but got " + o.getClass() + " instead"); } Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable<Void, IOException>() { @SuppressWarnings("unchecked") @Override public Void call() throws IOException { project.onLoad(project.getParent(), projectName); return null; } }); Jenkins.getActiveInstance().rebuildDependencyGraphAsync(); // if everything went well, commit this new version out.commit(); } finally { out.abort(); } } }