Java tutorial
/* * The MIT License * * Copyright (c) 2011-2017, CloudBees, Inc., Stephen Connolly. * * 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 jenkins.branch; import com.cloudbees.hudson.plugins.folder.ChildNameGenerator; import com.cloudbees.hudson.plugins.folder.FolderIcon; import com.cloudbees.hudson.plugins.folder.computed.ChildObserver; import com.cloudbees.hudson.plugins.folder.computed.ComputedFolder; import com.cloudbees.hudson.plugins.folder.computed.EventOutputStreams; import com.cloudbees.hudson.plugins.folder.computed.FolderComputation; import com.cloudbees.hudson.plugins.folder.views.AbstractFolderViewHolder; import com.google.common.collect.ImmutableSet; import com.thoughtworks.xstream.XStreamException; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.BulkChange; import hudson.Extension; import hudson.Functions; import hudson.Util; import hudson.XmlFile; import hudson.console.ModelHyperlinkNote; import hudson.model.Action; import hudson.model.Cause; import hudson.model.CauseAction; import hudson.model.Descriptor; import hudson.model.Failure; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.Items; import hudson.model.Job; import hudson.model.Run; import hudson.model.Saveable; import hudson.model.StreamBuildListener; import hudson.model.TaskListener; import hudson.model.TopLevelItem; import hudson.model.View; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; import hudson.scm.PollingResult; import hudson.security.ACL; import hudson.security.Permission; import hudson.util.PersistedList; import hudson.util.StreamTaskListener; import hudson.util.XStream2; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.servlet.ServletException; import jenkins.model.Jenkins; import jenkins.model.ParameterizedJobMixIn; import jenkins.model.TransientActionFactory; import jenkins.scm.api.SCMEvent; import jenkins.scm.api.SCMEventListener; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; import jenkins.scm.api.SCMHeadMigration; import jenkins.scm.api.SCMHeadObserver; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; import jenkins.scm.api.SCMSourceEvent; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.metadata.ObjectMetadataAction; import jenkins.scm.api.mixin.TagSCMHead; import jenkins.scm.impl.NullSCMSource; import jenkins.triggers.SCMTriggerItem; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; import org.apache.commons.io.Charsets; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.jenkins.ui.icon.IconSpec; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * Abstract base class for multiple-branch based projects. * * @param <P> the project type * @param <R> the run type */ public abstract class MultiBranchProject<P extends Job<P, R> & TopLevelItem, R extends Run<P, R>> extends ComputedFolder<P> implements SCMSourceOwner, IconSpec { /** * Our logger. */ private static final Logger LOGGER = Logger.getLogger(MultiBranchProject.class.getName()); /** * The user supplied branch sources. */ private /*almost final*/ PersistedList<BranchSource> sources = new BranchSourceList(this); /** * The persisted state maintained outside of the config file. * * @since 2.0 */ private transient /*almost final*/ State state = new State(this); /** * The source for dead branches. */ private transient /*almost final*/ NullSCMSource nullSCMSource; /** * The factory for building child job instances. */ private BranchProjectFactory<P, R> factory; private transient String srcDigest, facDigest; /** * Constructor, mandated by {@link TopLevelItem}. * * @param parent the parent of this multibranch job. * @param name the name of the multibranch job. */ protected MultiBranchProject(ItemGroup parent, String name) { super(parent, name); init2(); } /** * {@inheritDoc} */ @Override public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException { super.onLoad(parent, name); init2(); try { srcDigest = Util.getDigestOf(Items.XSTREAM2.toXML(sources)); } catch (XStreamException e) { srcDigest = null; } BranchProjectFactory<P, R> factory = getProjectFactory(); try { facDigest = Util.getDigestOf(Items.XSTREAM2.toXML(factory)); } catch (XStreamException e) { facDigest = null; } if (state == null) { state = new State(this); } try { state.load(); } catch (XStreamException | IOException e) { LOGGER.log(Level.WARNING, "Could not read persisted state, will be recovered on next index.", e); state.reset(); } // optimize lookup of sources by building a temporary map that is equivalent to getSCMSource(id) in results Map<String, SCMSource> sourceMap = new HashMap<>(); for (BranchSource source : sources) { SCMSource s = source.getSource(); String id = s.getId(); if (!sourceMap.containsKey(id)) { // only the first match should win sourceMap.put(id, s); } } for (P item : getItems()) { if (factory.isProject(item)) { Branch oldBranch = factory.getBranch(item); SCMSource source = sourceMap.get(oldBranch.getSourceId()); if (source == null || source instanceof NullSCMSource) { continue; } SCMHead oldHead = oldBranch.getHead(); SCMHead newHead = SCMHeadMigration.readResolveSCMHead(source, oldHead); if (newHead != oldHead) { LOGGER.log(Level.INFO, "Job {0}: a plugin upgrade is requesting migration of branch from {1} to {2}", new Object[] { item.getFullName(), oldHead.getClass(), newHead.getClass() }); try { Branch newBranch = new Branch(oldBranch.getSourceId(), newHead, oldBranch.getScm(), oldBranch.getProperties()); newBranch.setActions(oldBranch.getActions()); factory.setBranch(item, newBranch); SCMRevision revision = factory.getRevision(item); factory.setRevisionHash(item, SCMHeadMigration.readResolveSCMRevision(source, revision)); } catch (IOException | RuntimeException e) { LogRecord lr = new LogRecord(Level.WARNING, "Job {0}: Could not complete migration of branch from type {1} to {2}. " + "The side-effect of this is that the next index may trigger a rebuild " + "of the job (after which the issue will be resolved)"); lr.setThrown(e); lr.setParameters( new Object[] { item.getFullName(), oldHead.getClass(), newHead.getClass() }); LOGGER.log(lr); } } } } } /** * Consolidated initialization code. */ private synchronized void init2() { if (sources == null) { sources = new PersistedList<BranchSource>(this); } if (nullSCMSource == null) { nullSCMSource = new NullSCMSource(); } nullSCMSource.setOwner(this); for (SCMSource source : getSCMSources()) { source.setOwner(this); } final BranchProjectFactory<P, R> factory = getProjectFactory(); factory.setOwner(this); if (!(getFolderViews() instanceof MultiBranchProjectViewHolder)) { resetFolderViews(); } if (!(getIcon() instanceof MetadataActionFolderIcon)) { setIcon(newDefaultFolderIcon()); } } /** * {@inheritDoc} */ @Override protected AbstractFolderViewHolder newFolderViewHolder() { return new MultiBranchProjectViewHolder(this); } /** * {@inheritDoc} */ @Override protected FolderIcon newDefaultFolderIcon() { return new MetadataActionFolderIcon(); } /** * {@inheritDoc} */ @Override public String getIconClassName() { String result; if (sources.size() == 1) { result = sources.get(0).getSource().getDescriptor().getIconClassName(); } else { result = null; for (int i = 0; i < sources.size(); i++) { String iconClassName = sources.get(i).getSource().getDescriptor().getIconClassName(); if (i == 0) { result = iconClassName; } else if (!StringUtils.equals(result, iconClassName)) { result = null; break; } } } return result != null ? result : getDescriptor().getIconClassName(); } /** * {@inheritDoc} */ @Override public String getPronoun() { Set<String> result = new TreeSet<>(); for (BranchSource source : sources) { String pronoun = Util.fixEmptyAndTrim(source.getSource().getPronoun()); if (pronoun != null) { result.add(pronoun); } } return result.isEmpty() ? super.getPronoun() : StringUtils.join(result, " / "); } /** * Returns the {@link BranchProjectFactory}. * * @return the {@link BranchProjectFactory}. */ @NonNull public synchronized BranchProjectFactory<P, R> getProjectFactory() { if (factory == null) { setProjectFactory(newProjectFactory()); } return factory; } /** * Returns the base class of the projects that are managed by this {@link MultiBranchProject}. * * @return the base class of the projects that are managed by this {@link MultiBranchProject}. * @since 2.0 */ @SuppressWarnings("unchecked") public final Class<P> getProjectClass() { return (Class<P>) getDescriptor().getProjectClass(); } /** * Sets the {@link BranchProjectFactory}. * * @param projectFactory the new {@link BranchProjectFactory}. */ public synchronized void setProjectFactory(BranchProjectFactory<P, R> projectFactory) { projectFactory.getClass(); // throw NPE if null if (factory == projectFactory) { return; } if (factory != null) { factory.setOwner(null); } factory = projectFactory; factory.setOwner(this); } /** * Creates a new instance of the default project factory to be used for a new instance of the project type. * * @return a new default {@link BranchProjectFactory}. */ @NonNull protected abstract BranchProjectFactory<P, R> newProjectFactory(); /** * The sources of branches. * * @return the sources of branches. */ @NonNull public List<BranchSource> getSources() { if (sources != null) { return sources.toList(); } else { // return empty, this object is still being constructed return Collections.emptyList(); } } /** * Offers direct access to the configurable list of branch sources. * Intended for use from scripting and testing. * * @return the sources list. */ @NonNull public PersistedList<BranchSource> getSourcesList() { return sources; } /** * Offers direct access to set the configurable list of branch sources <strong>while</strong> preserving * branch source id associations for sources that are otherwise unmodified * * @param sources the new sources. * @throws IOException if the sources could not be persisted to disk. */ public void setSourcesList(List<BranchSource> sources) throws IOException { if (this.sources.isEmpty() || sources.isEmpty()) { // easy this.sources.replaceBy(sources); return; } Set<String> oldIds = sourceIds(this.sources); Set<String> newIds = sourceIds(sources); if (oldIds.containsAll(newIds) || newIds.containsAll(oldIds)) { // either adding, removing, or updating without an id change this.sources.replaceBy(sources); return; } // Now we need to check if any of the new entries are effectively the same as an old entry that is being removed // we will store the ID changes in a map and process all the affected branches to update their sourceIds Map<String, String> changedIds = new HashMap<>(); Set<String> additions = new HashSet<>(newIds); additions.removeAll(oldIds); Set<String> removals = new HashSet<>(oldIds); removals.removeAll(newIds); for (BranchSource addition : sources) { String additionId = addition.getSource().getId(); if (!additions.contains(additionId)) { continue; } for (BranchSource removal : this.sources) { String removalId = removal.getSource().getId(); if (!removals.contains(removalId)) { continue; } if (!equalButForId(removal.getSource(), addition.getSource())) { continue; } changedIds.put(removalId, additionId); // now take these two out of consideration removals.remove(removalId); additions.remove(additionId); break; } } this.sources.replaceBy(sources); BranchProjectFactory<P, R> factory = getProjectFactory(); for (P item : getItems()) { if (!factory.isProject(item)) { continue; } Branch oldBranch = factory.getBranch(item); if (changedIds.containsKey(oldBranch.getSourceId())) { Branch newBranch = new Branch(changedIds.get(oldBranch.getSourceId()), oldBranch.getHead(), oldBranch.getScm(), oldBranch.getProperties()); newBranch.setActions(oldBranch.getActions()); factory.setBranch(item, newBranch); } } } private Set<String> sourceIds(List<BranchSource> sources) { Set<String> result = new HashSet<>(); for (BranchSource s : sources) { result.add(s.getSource().getId()); } return result; } private boolean equalButForId(SCMSource a, SCMSource b) { if (!a.getClass().equals(b.getClass())) { return false; } return SOURCE_ID_OMITTED_XSTREAM.toXML(a).equals(SOURCE_ID_OMITTED_XSTREAM.toXML(b)); } private final static XStream2 SOURCE_ID_OMITTED_XSTREAM = new XStream2(); static { SOURCE_ID_OMITTED_XSTREAM.omitField(SCMSource.class, "id"); } /** * {@inheritDoc} */ @NonNull public List<SCMSource> getSCMSources() { List<SCMSource> result = new ArrayList<SCMSource>(); if (sources != null) { for (BranchSource source : sources) { result.add(source.getSource()); } } // else, ok to ignore and return empty, this object is still being constructed return result; } /** * {@inheritDoc} */ @CheckForNull public SCMSource getSCMSource(@CheckForNull String sourceId) { for (SCMSource source : getSCMSources()) { if (source.getId().equals(sourceId)) { return source; } } return nullSCMSource; } /** * Returns the {@link BranchPropertyStrategy} for a specific {@link SCMSource}. * * @param source the specific {@link SCMSource}. * @return the {@link BranchPropertyStrategy} to use. */ @CheckForNull public BranchPropertyStrategy getBranchPropertyStrategy(@NonNull SCMSource source) { for (BranchSource s : getSources()) { if (s.getSource().equals(source)) { return s.getStrategy(); } } return null; } /** * Creates a {@link Branch} for a specific {@link SCMSource} and {@link SCMHead}. * * @param source the {@link SCMSource} * @param head the {@link SCMHead}. * @return the {@link Branch} */ @NonNull private Branch newBranch(@NonNull SCMSource source, @NonNull SCMHead head) { source.getClass(); // throw NPE if null head.getClass(); // throw NPE if null String sourceId = source.getId(); if (NullSCMSource.ID.equals(sourceId)) { return new Branch.Dead(head, Collections.<BranchProperty>emptyList()); } else { final BranchPropertyStrategy strategy = getBranchPropertyStrategy(source); return new Branch(sourceId, head, source.build(head), strategy != null ? strategy.getPropertiesFor(head) : Collections.<BranchProperty>emptyList()); } } /** * {@inheritDoc} */ @CheckForNull public SCMSourceCriteria getSCMSourceCriteria(@NonNull SCMSource source) { return null; } /** * {@inheritDoc} */ public void onSCMSourceUpdated(@NonNull SCMSource source) { if (isBuildable()) { scheduleBuild(0, new BranchIndexingCause()); } } /** * {@inheritDoc} */ @Override public boolean isHasEvents() { return true; } /** * {@inheritDoc} */ @Override protected void computeChildren(final ChildObserver<P> observer, final TaskListener listener) throws IOException, InterruptedException { // capture the current digests to prevent unnecessary reindex if re-saving after index try { srcDigest = Util.getDigestOf(Items.XSTREAM2.toXML(sources)); } catch (XStreamException e) { srcDigest = null; } try { facDigest = Util.getDigestOf(Items.XSTREAM2.toXML(getProjectFactory())); } catch (XStreamException e) { facDigest = null; } long start = System.currentTimeMillis(); listener.getLogger().format("[%tc] Starting branch indexing...%n", start); try { final BranchProjectFactory<P, R> _factory = getProjectFactory(); List<SCMSource> scmSources = getSCMSources(); Map<String, List<Action>> sourceActions = new LinkedHashMap<>(); for (SCMSource source : scmSources) { try { sourceActions.put(source.getId(), source.fetchActions(null, listener)); } catch (IOException | InterruptedException | RuntimeException e) { printStackTrace(e, listener.error("[%tc] Could not update folder level actions from source %s", System.currentTimeMillis(), source.getId())); throw e; } } // update any persistent actions for the SCMSource if (!sourceActions.equals(state.sourceActions)) { boolean saveProject = false; for (List<Action> actions : sourceActions.values()) { for (Action a : actions) { // undo any hacks that attached the contributed actions without attribution saveProject = removeActions(a.getClass()) || saveProject; } } BulkChange bc = new BulkChange(state); try { state.sourceActions.keySet().retainAll(sourceActions.keySet()); state.sourceActions.putAll(sourceActions); try { bc.commit(); } catch (IOException | RuntimeException e) { printStackTrace(e, listener.error("[%tc] Could not persist folder level actions", System.currentTimeMillis())); throw e; } if (saveProject) { try { save(); } catch (IOException | RuntimeException e) { printStackTrace(e, listener.error("[%tc] Could not persist folder level configuration changes", System.currentTimeMillis())); throw e; } } } finally { bc.abort(); } } for (final SCMSource source : scmSources) { try { source.fetch(new SCMHeadObserverImpl(source, observer, listener, _factory, new IndexingCauseFactory(), null), listener); } catch (IOException | InterruptedException | RuntimeException e) { printStackTrace(e, listener.error("[%tc] Could not fetch branches from source %s", System.currentTimeMillis(), source.getId())); throw e; } } } finally { long end = System.currentTimeMillis(); listener.getLogger().format("[%tc] Finished branch indexing. Indexing took %s%n", end, Util.getTimeSpanString(end - start)); } } private void scheduleBuild(BranchProjectFactory<P, R> factory, final P item, SCMRevision revision, TaskListener listener, String name, Cause[] cause, Action... actions) { if (!isBuildable()) { listener.getLogger().printf("Did not schedule build for branch: %s (%s is disabled)%n", name, getDisplayName()); return; } Action[] _actions = new Action[actions.length + 1]; _actions[0] = new CauseAction(cause); System.arraycopy(actions, 0, _actions, 1, actions.length); if (ParameterizedJobMixIn.scheduleBuild2(item, 0, _actions) != null) { listener.getLogger().println("Scheduled build for branch: " + name); try { factory.setRevisionHash(item, revision); } catch (IOException e) { printStackTrace(e, listener.error("Could not update last revision hash")); } } else { listener.getLogger().println("Did not schedule build for branch: " + name); } } /** * {@inheritDoc} */ @Override protected Collection<P> orphanedItems(Collection<P> orphaned, TaskListener listener) throws IOException, InterruptedException { BranchProjectFactory<P, R> _factory = getProjectFactory(); for (P project : orphaned) { if (!_factory.isProject(project)) { listener.getLogger().println("Detected unsupported subitem " + project + ", skipping"); continue; // TODO perhaps better to remove from list passed to super, and return it from here } Branch b = _factory.getBranch(project); if (!(b instanceof Branch.Dead)) { _factory.decorate(_factory.setBranch(project, new Branch.Dead(b))); } } return super.orphanedItems(orphaned, listener); } /** * Returns the named child job or {@code null} if no such job exists. * * @param name the name of the child job. * @return the named child job or {@code null} if no such job exists. */ @Override @CheckForNull public P getItem(String name) { if (name == null) { return null; } P item = super.getItem(name); if (item != null) { return item; } if (name.indexOf('%') != -1) { String decoded = NameEncoder.decode(name); item = super.getItem(decoded); if (item != null) { return item; } // fall through for double decoded call paths // TODO is this necessary } return super.getItem(NameEncoder.encode(name)); } /** * Returns the child job with the specified branch name or {@code null} if no such child job exists. * * @param branchName the name of the branch. * @return the child job or {@code null} if no such job exists or if the requesting user does ave permission to * view it. * @since 2.0.0 */ @CheckForNull public P getItemByBranchName(@NonNull String branchName) { return super.getItem(NameEncoder.encode(branchName)); } /** * Returns the named branch job or {@code null} if no such branch exists. * * @param name the name of the branch * @return the named branch job or {@code null} if no such branch exists. * @deprecated use {@link #getItem(String)} or {@link #getJob(String)} directly */ @Deprecated @CheckForNull @SuppressWarnings("unused") // by stapler for URL binding public P getBranch(String name) { return getItem(name); } /** * {@inheritDoc} */ @Override public ACL getACL() { final ACL acl = super.getACL(); if (getParent() instanceof OrganizationFolder) { return new ACL() { @Override public boolean hasPermission(Authentication a, Permission permission) { if (ACL.SYSTEM.equals(a)) { return true; } else if (SUPPRESSED_PERMISSIONS.contains(permission)) { return false; } else { return acl.hasPermission(a, permission); } } }; } else { return acl; } } private static final Set<Permission> SUPPRESSED_PERMISSIONS = ImmutableSet.of(Item.CONFIGURE, Item.DELETE, View.CONFIGURE, View.CREATE, View.DELETE); /** * {@inheritDoc} */ @Override public String getDescription() { String description = super.getDescription(); if (StringUtils.isNotBlank(description)) { return description; } ObjectMetadataAction action = getAction(ObjectMetadataAction.class); if (action != null) { return action.getObjectDescription(); } return super.getDescription(); } /** * {@inheritDoc} */ @Override public String getDisplayName() { String displayName = getDisplayNameOrNull(); if (displayName == null) { ObjectMetadataAction action = getAction(ObjectMetadataAction.class); if (action != null && StringUtils.isNotBlank(action.getObjectDisplayName())) { return action.getObjectDisplayName(); } } return super.getDisplayName(); } /** * Returns the directory that all branches are stored in. * * @return the directory that all branches are stored in. */ @Override @NonNull public File getJobsDir() { return new File(getRootDir(), "branches"); } /** * {@inheritDoc} */ @Override public File getRootDirFor(P child) { return super.getRootDirFor(child); // need the bridge method to provide binary compatibility } /** * Returns the directory that branch indexing is stored in. * * @return the directory that branch indexing is stored in. */ @Override @NonNull public File getComputationDir() { return new File(getRootDir(), "indexing"); } /** * {@inheritDoc} */ @Override @NonNull public MultiBranchProjectDescriptor getDescriptor() { return (MultiBranchProjectDescriptor) super.getDescriptor(); } /** * Returns the current/most recent indexing details. * * @return the current/most recent indexing details. */ @SuppressWarnings("unused") // URL binding for stapler. public synchronized BranchIndexing<P, R> getIndexing() { return (BranchIndexing) getComputation(); } /** * {@inheritDoc} */ @Override protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException { super.submit(req, rsp); List<SCMSource> _sources = new ArrayList<>(); synchronized (this) { JSONObject json = req.getSubmittedForm(); setSourcesList(req.bindJSONToList(BranchSource.class, json.opt("sources"))); for (SCMSource scmSource : getSCMSources()) { scmSource.setOwner(this); _sources.add(scmSource); } setProjectFactory(req.bindJSON(BranchProjectFactory.class, json.getJSONObject("projectFactory"))); } for (SCMSource scmSource : _sources) { scmSource.afterSave(); } String srcDigest; try { srcDigest = Util.getDigestOf(Items.XSTREAM2.toXML(sources)); } catch (XStreamException e) { srcDigest = null; } String facDigest; try { facDigest = Util.getDigestOf(Items.XSTREAM2.toXML(getProjectFactory())); } catch (XStreamException e) { facDigest = null; } recalculateAfterSubmitted(!StringUtils.equals(srcDigest, this.srcDigest)); recalculateAfterSubmitted(!StringUtils.equals(facDigest, this.facDigest)); this.srcDigest = srcDigest; this.facDigest = facDigest; } /** * Creates a place-holder view when there's no active branch indexed. * * @return the place-holder view when there's no active branch indexed. */ protected View getWelcomeView() { return new MultiBranchProjectEmptyView(this); } /** * {@inheritDoc} */ @Override public View getView(String name) { if (name.equals("Welcome")) { return getWelcomeView(); } else { return super.getView(name); } } /** * Represents the branch indexing job. * * @param <P> the type of project that the branch projects consist of. * @param <R> the type of runs that the branch projects use. */ public static class BranchIndexing<P extends Job<P, R> & TopLevelItem, R extends Run<P, R>> extends FolderComputation<P> { public BranchIndexing(@NonNull MultiBranchProject<P, R> project, @CheckForNull BranchIndexing<P, R> previousIndexing) { super(project, previousIndexing); } /** * {@inheritDoc} */ @Override public MultiBranchProject<P, R> getParent() { return (MultiBranchProject) super.getParent(); } /** * {@inheritDoc} */ @Override protected XmlFile getDataFile() { return new XmlFile(Items.XSTREAM, new File(getParent().getComputationDir(), "indexing.xml")); } /** * {@inheritDoc} */ @Override public File getLogFile() { return new File(getParent().getComputationDir(), "indexing.log"); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return Messages.MultiBranchProject_BranchIndexing_displayName(getParent().getPronoun()); } /** * {@inheritDoc} */ @Override public String getUrl() { return getParent().getUrl() + "indexing/"; } /** * {@inheritDoc} */ @Override public String getSearchUrl() { return "indexing/"; } @Override public void run() { long start = System.currentTimeMillis(); try { super.run(); } finally { long end = System.currentTimeMillis(); LOGGER.log(Level.INFO, "{0} #{1,time,yyyyMMdd.HHmmss} branch indexing action completed: {2} in {3}", new Object[] { getParent().getFullName(), start, getResult(), Util.getTimeSpanString(end - start) }); } } } /** * {@inheritDoc} */ @Override public boolean isBuildable() { if (sources == null) { return false; // still loading } return super.isBuildable() && !sources.isEmpty(); } /** * {@inheritDoc} */ @Override protected FolderComputation<P> createComputation(FolderComputation<P> previous) { return new BranchIndexing<P, R>(this, (BranchIndexing) previous); } /** * Inverse function of {@link Util#rawEncode(String)} * * @param s the encoded string. * @return the decoded string. */ @NonNull public static String rawDecode(@NonNull String s) { final byte[] bytes; // should be US-ASCII but we can be tolerant try { bytes = s.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("JLS specification mandates UTF-8 as a supported encoding", e); } final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); for (int i = 0; i < bytes.length; i++) { final int b = bytes[i]; if (b == '%' && i + 2 < bytes.length) { final int u = Character.digit((char) bytes[++i], 16); final int l = Character.digit((char) bytes[++i], 16); if (u != -1 && l != -1) { buffer.write((char) ((u << 4) + l)); continue; } // should be a valid encoding but we can be tolerant i -= 2; } buffer.write(b); } try { return new String(buffer.toByteArray(), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("JLS specification mandates UTF-8 as a supported encoding", e); } } private static class BranchSourceList extends PersistedList<BranchSource> { BranchSourceList(MultiBranchProject<?, ?> owner) { super(owner); } /** * {@inheritDoc} */ @Override protected void onModified() throws IOException { super.onModified(); for (BranchSource branchSource : this) { branchSource.getSource().setOwner((MultiBranchProject) owner); } } } /** * Our event listener. */ @Extension public static class SCMEventListenerImpl extends SCMEventListener { private final EventOutputStreams globalEvents = createGlobalEvents(); private EventOutputStreams createGlobalEvents() { File logsDir = new File(Jenkins.getActiveInstance().getRootDir(), "logs"); if (!logsDir.isDirectory() && !logsDir.mkdirs()) { LOGGER.log(Level.WARNING, "Could not create logs directory: {0}", logsDir); } final File eventsFile = new File(logsDir, MultiBranchProject.class.getName() + ".log"); if (!eventsFile.isFile()) { File oldFile = new File(logsDir.getParent(), eventsFile.getName()); if (oldFile.isFile()) { if (!oldFile.renameTo(eventsFile)) { FileUtils.deleteQuietly(oldFile); } } } return new EventOutputStreams(new EventOutputStreams.OutputFile() { @NonNull @Override public File get() { return eventsFile; } }, 250, TimeUnit.MILLISECONDS, 1024, true, 32 * 1024, 5); } /** * The {@link TaskListener} for events that we cannot assign to a multi-branch project. * * @return The {@link TaskListener} for events that we cannot assign to a multi-branch project. */ @Restricted(NoExternalUse.class) public StreamTaskListener globalEventsListener() { return new StreamBuildListener(globalEvents.get(), Charsets.UTF_8); } /** * {@inheritDoc} */ @Override public void onSCMHeadEvent(final SCMHeadEvent<?> event) { try (StreamTaskListener global = globalEventsListener()) { String eventDescription = StringUtils.defaultIfBlank(event.description(), event.getClass().getName()); String eventType = event.getType().name(); String eventOrigin = event.getOrigin(); long eventTimestamp = event.getTimestamp(); global.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n", System.currentTimeMillis(), eventDescription, eventType, eventOrigin, eventTimestamp); LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: onSCMHeadEvent", new Object[] { eventDescription, eventType, eventTimestamp }); int matchCount = 0; // not interested in removal of dead items as that needs to be handled by the computation in order // to ensure that other sources do not want to take ownership and also to ensure that the dead branch // strategy is enforced correctly try { if (SCMEvent.Type.CREATED == event.getType()) { matchCount = processHeadCreate(event, global, eventDescription, eventType, eventOrigin, eventTimestamp, matchCount); } else if (SCMEvent.Type.UPDATED == event.getType() || SCMEvent.Type.REMOVED == event.getType()) { matchCount = processHeadUpdate(event, global, eventDescription, eventType, eventOrigin, eventTimestamp, matchCount); } } catch (InterruptedException e) { printStackTrace(e, global.error( "[%tc] Interrupted while processing %s %s event from %s with timestamp %tc", System.currentTimeMillis(), eventDescription, eventType, eventOrigin, eventTimestamp)); } global.getLogger().format( "[%tc] Finished processing %s %s event from %s with timestamp %tc. Matched %d.%n", System.currentTimeMillis(), eventDescription, eventType, eventOrigin, eventTimestamp, matchCount); } catch (IOException e) { LOGGER.log(Level.WARNING, "Could not close global event log file", e); } } private int processHeadCreate(SCMHeadEvent<?> event, TaskListener global, String eventDescription, String eventType, String eventOrigin, long eventTimestamp, int matchCount) throws IOException, InterruptedException { Set<String> sourceIds = new HashSet<>(); for (MultiBranchProject<?, ?> p : Jenkins.getActiveInstance().getAllItems(MultiBranchProject.class)) { String pFullName = p.getFullName(); if (!p.isBuildable()) { LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Ignoring {3} because it is disabled", new Object[] { eventDescription, eventType, eventTimestamp, pFullName }); continue; } sourceIds.clear(); LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Checking {3} for a match", new Object[] { eventDescription, eventType, eventTimestamp, pFullName }); boolean haveMatch = false; final BranchProjectFactory _factory = p.getProjectFactory(); SOURCES: for (SCMSource source : p.getSCMSources()) { if (event.isMatch(source)) { LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Project {3}: Matches source {4}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId() }); for (SCMHead h : event.heads(source).keySet()) { String name = h.getName(); Job job = p.getItemByBranchName(name); if (job == null) { LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Project {3}: Source {4}: Match: {5}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId(), name }); // only interested in create events that actually could create a new branch haveMatch = true; matchCount++; global.getLogger().format("Found match against %s (new branch %s)%n", pFullName, name); break SOURCES; } Branch branch = _factory.getBranch(job); if (branch instanceof Branch.Dead) { LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Project {3}: Source {4}: Match: {5}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId(), name }); // only interested in create events that actually could create a new branch haveMatch = true; matchCount++; global.getLogger().format("Found match against %s (resurrect branch %s)%n", pFullName, name); break SOURCES; } String sourceId = branch.getSourceId(); if (StringUtils.equals(sourceId, source.getId())) { LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Project {3}: Source {4}: Already have: {5}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId(), name }); continue; } if (sourceIds.contains(sourceId)) { LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Project {3}: Source {4}: Ignored as " + "already have {5} from higher priority source {6}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId(), name, sourceId }); continue; } LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Project {3}: Source {4}: Match: {5} " + "overriding lower priority source {6}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId(), name, sourceId }); // only interested in create events that actually could create a new branch haveMatch = true; matchCount++; global.getLogger().format("Found match against %s (takeover branch %s)%n", pFullName, name); break SOURCES; } LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Project {3}: No new projects for {4}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId() }); } else { LOGGER.log(Level.FINEST, "{0} {1} {2,date} {2,time}: Project {3}: Does not matches source {4}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, source.getId() }); } sourceIds.add(source.getId()); } if (haveMatch) { long start = System.currentTimeMillis(); try (StreamTaskListener listener = p.getComputation().createEventsListener(); ChildObserver childObserver = p.openEventsChildObserver()) { try { listener.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n", start, eventDescription, eventType, eventOrigin, eventTimestamp); for (SCMSource source : p.getSCMSources()) { if (event.isMatch(source)) { source.fetch(p.getSCMSourceCriteria(source), p.new SCMHeadObserverImpl(source, childObserver, listener, _factory, new EventCauseFactory(event), event), event, listener); } } } catch (IOException e) { printStackTrace(e, listener.error(e.getMessage())); } catch (InterruptedException e) { printStackTrace(e, listener.error(e.getMessage())); throw e; } finally { long end = System.currentTimeMillis(); listener.getLogger().format( "[%tc] %s %s event from %s with timestamp %tc processed in %s%n", end, eventDescription, eventType, eventOrigin, eventTimestamp, Util.getTimeSpanString(end - start)); } } catch (IOException e) { printStackTrace(e, global.error( "[%tc] %s encountered an error while processing %s %s event from %s with timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, eventType, eventOrigin, eventTimestamp)); } catch (InterruptedException e) { printStackTrace(e, global.error( "[%tc] %s was interrupted while processing %s %s event from %s with timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, eventType, eventOrigin, eventTimestamp)); throw e; } } } return matchCount; } private int processHeadUpdate(SCMHeadEvent<?> event, TaskListener global, String eventDescription, String eventType, String eventOrigin, long eventTimestamp, int matchCount) throws InterruptedException { Map<SCMSource, SCMHead> matches = new IdentityHashMap<>(); Set<String> candidateNames = new HashSet<>(); Map<SCMSource, Map<SCMHead, SCMRevision>> revisionMaps = new IdentityHashMap<>(); Set<Job<?, ?>> jobs = new HashSet<>(); for (MultiBranchProject<?, ?> p : Jenkins.getActiveInstance().getAllItems(MultiBranchProject.class)) { String pFullName = p.getFullName(); if (!p.isBuildable()) { LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Ignoring {3} because it is disabled", new Object[] { eventDescription, eventType, eventTimestamp, pFullName }); continue; } LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Checking {3} for a match", new Object[] { eventDescription, eventType, eventTimestamp, pFullName }); final BranchProjectFactory _factory = p.getProjectFactory(); matches.clear(); candidateNames.clear(); revisionMaps.clear(); for (SCMSource source : p.getSCMSources()) { if (event.isMatch(source)) { Map<SCMHead, SCMRevision> eventHeads = event.heads(source); if (!eventHeads.isEmpty()) { revisionMaps.put(source, eventHeads); for (SCMHead h : eventHeads.keySet()) { candidateNames.add(h.getName()); } } } } jobs.clear(); for (Job i : p.getItems()) { if (_factory.isProject(i)) { Branch branch = _factory.getBranch(i); if (!candidateNames.contains(branch.getName())) { continue; } if (branch instanceof Branch.Dead) { LOGGER.log(Level.FINEST, "{0} {1} {2,date} {2,time}: Checking {3} -> Resurrect dead " + "branch {4} (job {5})?", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, branch.getName(), i.getName() }); // try to bring back from the dead by checking all sources in sequence // the first to match the event and that contains the head wins! SOURCES: for (SCMSource src : p.getSCMSources()) { Map<SCMHead, SCMRevision> revisionMap = revisionMaps.get(src); if (revisionMap == null) { continue; } SCMHead head = branch.getHead(); for (SCMHead h : revisionMap.keySet()) { // for bringing back from the dead we need to check the name as it could be // back from the dead on a different source from original if (h.getName().equals(head.getName())) { matches.put(src, head); jobs.add(i); break SOURCES; } } } } else { // TODO takeover LOGGER.log(Level.FINEST, "{0} {1} {2,date} {2,time}: Checking {3} -> Matches existing branch {4} " + "(job {5})?", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, branch.getName(), i.getName() }); SCMSource src = p.getSCMSource(branch.getSourceId()); if (src == null) { continue; } Map<SCMHead, SCMRevision> revisionMap = revisionMaps.get(src); if (revisionMap == null) { // we got here because a source matches the name // need to find the source that did match the name and compare priorities // to see if this is a takeover LOGGER.log(Level.FINEST, "{0} {1} {2,date} {2,time}: Checking {3} -> Event does not match current " + "source of {4} (job {5}), checking for take-over", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, branch.getName(), i.getName() }); // check who has priority int ourPriority = Integer.MAX_VALUE; int oldPriority = Integer.MAX_VALUE; SCMSource ourSource = null; int priority = 1; for (SCMSource s : p.getSCMSources()) { String sId = s.getId(); Map<SCMHead, SCMRevision> rMap = revisionMaps.get(s); if (ourPriority > priority && oldPriority > priority && rMap != null) { // only need to check for takeover when the event is higher priority for (SCMHead h : rMap.keySet()) { if (branch.getName().equals(h.getName())) { ourPriority = priority; ourSource = s; break; } } } if (sId.equals(src.getId())) { oldPriority = priority; } priority++; } if (oldPriority < ourPriority) { LOGGER.log(Level.FINEST, "{0} {1} {2,date} {2,time}: Checking {3} -> Ignoring event for {4} " + "(job {5}) from source #{6} as source #{7} owns the branch name", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, branch.getName(), i.getName(), ourPriority, oldPriority }); continue; } else { LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Checking {3} -> Takeover event for {4} " + "(job {5}) by source #{5} from source #{6}", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, branch.getName(), i.getName(), ourPriority, oldPriority }); assert ourSource != null; src = ourSource; revisionMap = revisionMaps.get(ourSource); assert revisionMap != null; } } SCMHead head = branch.getHead(); // The distinguishing key for branch projects is the name, so check on the name boolean match = false; for (SCMHead h : revisionMap.keySet()) { if (h.getName().equals(head.getName())) { match = true; break; } } if (match) { LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Checking {3} -> Event matches source of {4} " + "(job {5})", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, branch.getName(), i.getName() }); if (SCMEvent.Type.UPDATED == event.getType()) { SCMRevision revision = revisionMap.get(head); if (revision != null && revision.isDeterministic()) { SCMRevision lastBuild = _factory.getRevision(i); if (revision.equals(lastBuild)) { // we are not interested in events that tell us a revision // we have already seen LOGGER.log(Level.FINE, "{0} {1} {2,date} {2,time}: Checking {3} -> Ignoring event as " + "revision {4} is same as last build of {5} (job {6})", new Object[] { eventDescription, eventType, eventTimestamp, pFullName, revision, branch.getName(), i.getName() }); continue; } } } matches.put(src, head); jobs.add(i); } } } } if (!matches.isEmpty()) { matchCount++; global.getLogger().format("Found match against %s%n", pFullName); long start = System.currentTimeMillis(); try (StreamTaskListener listener = p.getComputation().createEventsListener(); ChildObserver childObserver = p.openEventsChildObserver()) { try { listener.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n", start, eventDescription, eventType, eventOrigin, eventTimestamp); for (Map.Entry<SCMSource, SCMHead> m : matches.entrySet()) { m.getKey() .fetch(p.getSCMSourceCriteria(m.getKey()), p.new SCMHeadObserverImpl(m.getKey(), childObserver, listener, _factory, new EventCauseFactory(event), event), event, listener); } // now dis-associate branches that no-longer exist Set<String> names = childObserver.observed(); for (Job<?, ?> j : jobs) { if (names.contains(j.getName())) { // observed, so not dead continue; } Branch branch = _factory.getBranch(j); String sourceId = branch.getSourceId(); boolean foundSource = false; for (SCMSource s : matches.keySet()) { if (sourceId.equals(s.getId())) { foundSource = true; } } if (!foundSource) { // not safe to switch to a dead branch continue; } _factory.decorate(_factory.setBranch(j, new Branch.Dead(branch))); j.save(); } } catch (IOException e) { printStackTrace(e, listener.error(e.getMessage())); } catch (InterruptedException e) { printStackTrace(e, listener.error(e.getMessage())); throw e; } finally { long end = System.currentTimeMillis(); listener.getLogger().format( "[%tc] %s %s event from %s with timestamp %tc processed in %s%n", end, eventDescription, eventType, eventOrigin, eventTimestamp, Util.getTimeSpanString(end - start)); } } catch (IOException e) { printStackTrace(e, global.error( "[%tc] %s encountered an error while processing %s %s event from %s with timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, eventType, eventOrigin, eventTimestamp)); } catch (InterruptedException e) { printStackTrace(e, global.error( "[%tc] %s was interrupted while processing %s %s event from %s with timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, eventType, eventOrigin, eventTimestamp)); throw e; } } else { // didn't match an existing branch, maybe the criteria now match against an updated branch boolean haveMatch = false; for (SCMSource source : p.getSCMSources()) { if (event.isMatch(source)) { for (SCMHead h : event.heads(source).keySet()) { if (p.getItemByBranchName(h.getName()) == null) { // only interested in create events that actually could create a new branch haveMatch = true; break; } } if (haveMatch) { matchCount++; global.getLogger().format("Found match against %s%n", pFullName); break; } break; } } if (haveMatch) { long start = System.currentTimeMillis(); try (StreamTaskListener listener = p.getComputation().createEventsListener(); ChildObserver childObserver = p.openEventsChildObserver()) { listener.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n", start, eventDescription, eventType, eventOrigin, eventTimestamp); try { for (SCMSource source : p.getSCMSources()) { if (event.isMatch(source)) { source.fetch(p.getSCMSourceCriteria(source), p.new SCMHeadObserverImpl(source, childObserver, listener, _factory, new EventCauseFactory(event), event), event, listener); } } } catch (IOException e) { printStackTrace(e, listener.error(e.getMessage())); } catch (InterruptedException e) { printStackTrace(e, listener.error(e.getMessage())); throw e; } finally { long end = System.currentTimeMillis(); listener.getLogger().format( "[%tc] %s %s event from %s with timestamp %tc processed in %s%n", end, eventDescription, eventType, eventOrigin, eventTimestamp, Util.getTimeSpanString(end - start)); } } catch (IOException e) { printStackTrace(e, global.error( "[%tc] %s encountered an error while processing %s %s event from %s with " + "timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, eventType, eventOrigin, eventTimestamp)); } catch (InterruptedException e) { printStackTrace(e, global.error( "[%tc] %s was interrupted while processing %s %s event from %s with " + "timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, eventType, eventOrigin, eventTimestamp)); throw e; } } } } return matchCount; } /** * {@inheritDoc} */ @Override public void onSCMSourceEvent(SCMSourceEvent<?> event) { try (StreamTaskListener global = globalEventsListener()) { String eventDescription = StringUtils.defaultIfBlank(event.description(), event.getClass().getName()); global.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n", System.currentTimeMillis(), eventDescription, event.getType().name(), event.getOrigin(), event.getTimestamp()); int matchCount = 0; // not interested in creation as that is an event for org folders // not interested in removal as that is an event for org folders if (SCMEvent.Type.UPDATED == event.getType()) { // we are only interested in updates as they would trigger the actions being updated try { for (MultiBranchProject<?, ?> p : Jenkins.getActiveInstance() .getAllItems(MultiBranchProject.class)) { if (!p.isBuildable()) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "{0} {1} {2,date} {2,time}: Ignoring {3} because it is disabled", new Object[] { eventDescription, event.getType().name(), event.getTimestamp(), p.getFullName() }); } continue; } boolean haveMatch = false; List<SCMSource> scmSources = p.getSCMSources(); for (SCMSource s : scmSources) { if (event.isMatch(s)) { matchCount++; global.getLogger().format("Found match against %s%n", p.getFullName()); haveMatch = true; break; } } if (haveMatch) { try (StreamTaskListener listener = p.getComputation().createEventsListener()) { try { Map<String, List<Action>> stateActions = new HashMap<>(); for (SCMSource source : scmSources) { List<Action> oldActions = p.state.sourceActions.get(source.getId()); List<Action> newActions; try { newActions = source.fetchActions(event, listener); } catch (IOException e) { printStackTrace(e, listener.error( "Could not refresh actions for source %s", source.getId())); // preserve previous actions if we have some transient error fetching now (e.g. // API rate limit) newActions = oldActions; } if (oldActions == null || !oldActions.equals(newActions)) { stateActions.put(source.getId(), newActions); } } if (!stateActions.isEmpty()) { boolean saveProject = false; for (List<Action> actions : stateActions.values()) { for (Action a : actions) { // undo any hacks that attached the contributed actions without // attribution saveProject = p.removeActions(a.getClass()) || saveProject; } } BulkChange bc = new BulkChange(p.state); try { p.state.sourceActions.putAll(stateActions); bc.commit(); if (saveProject) { p.save(); } } finally { bc.abort(); } } } catch (IOException e) { printStackTrace(e, listener.error(e.getMessage())); } catch (InterruptedException e) { printStackTrace(e, listener.error(e.getMessage())); throw e; } } catch (IOException e) { printStackTrace(e, global.error( "[%tc] %s encountered an error while processing %s %s event from %s with " + "timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, event.getType().name(), event.getOrigin(), event.getTimestamp())); } catch (InterruptedException e) { printStackTrace(e, global.error( "[%tc] %s was interrupted while processing %s %s event from %s with " + "timestamp %tc", System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p), eventDescription, event.getType().name(), event.getOrigin(), event.getTimestamp())); throw e; } } } } catch (InterruptedException e) { printStackTrace(e, global.error( "[%tc] Interrupted while processing %s %s event from %s with timestamp %tc", System.currentTimeMillis(), eventDescription, event.getType().name(), event.getOrigin(), event.getTimestamp())); } } global.getLogger().format( "[%tc] Finished processing %s %s event from %s with timestamp %tc. Matched %d.%n", System.currentTimeMillis(), eventDescription, event.getType().name(), event.getOrigin(), event.getTimestamp(), matchCount); } catch (IOException e) { LOGGER.log(Level.WARNING, "Could not close global event log file", e); } } } /** * We need to be able to inject causes which may include causes contributed by the triggering event. */ private static abstract class CauseFactory { /** * Create the cause instances on demand. The instances in the array probably need to be new instances * for each build as we cannot know the assumptions that plugin authors have made in * {@link Cause#onAddedTo(Run)}. * * @return an array of new cause instances. * @param source */ @NonNull abstract Cause[] create(SCMSource source); } /** * A cause factory for {@link BranchIndexingCause}. */ private static class IndexingCauseFactory extends CauseFactory { /** * {@inheritDoc} * @param source */ @NonNull @Override Cause[] create(SCMSource source) { return new Cause[] { new BranchIndexingCause() }; } } /** * A cause factory for {@link BranchEventCause}. */ private static class EventCauseFactory extends CauseFactory { /** * The event. */ @NonNull private final SCMHeadEvent<?> event; EventCauseFactory(SCMHeadEvent<?> event) { this.event = event; } /** * {@inheritDoc} * @param source */ @NonNull @Override Cause[] create(SCMSource source) { Cause[] eventCauses = event.asCauses(); Cause[] result = new Cause[eventCauses.length + 1]; result[0] = new BranchEventCause(event, event.descriptionFor(source)); if (eventCauses.length > 0) { System.arraycopy(eventCauses, 0, result, 1, eventCauses.length); } return result; } } /** * Our observer. */ private class SCMHeadObserverImpl extends SCMHeadObserver { /** * The source that we are observing. */ @NonNull private final SCMSource source; /** * The child observer. */ @NonNull private final ChildObserver<P> observer; /** * The task listener. */ @NonNull private final TaskListener listener; /** * The project factory. */ @NonNull private final BranchProjectFactory<P, R> _factory; /** * A source of {@link Cause} instances to use when triggering builds. */ @NonNull private final CauseFactory causeFactory; /** * The optional event to use when scoping queries. */ @CheckForNull private final SCMHeadEvent<?> event; /** * Constructor. * * @param source The source that we are observing. * @param observer The child observer. * @param listener The task listener. * @param _factory The project factory. * @param causeFactory A source of {@link Cause} instances to use when triggering builds. * @param event The optional event to use when scoping queries. */ public SCMHeadObserverImpl(@NonNull SCMSource source, @NonNull ChildObserver<P> observer, @NonNull TaskListener listener, @NonNull BranchProjectFactory<P, R> _factory, @NonNull CauseFactory causeFactory, @CheckForNull SCMHeadEvent<?> event) { this.source = source; this.observer = observer; this.listener = listener; this._factory = _factory; this.causeFactory = causeFactory; this.event = event; } /** * {@inheritDoc} */ @Override public void observe(@NonNull SCMHead head, @NonNull SCMRevision revision) throws IOException, InterruptedException { Branch branch = newBranch(source, head); String rawName = branch.getName(); String encodedName = branch.getEncodedName(); P project = observer.shouldUpdate(encodedName); try { Branch origBranch; if (project == null) { origBranch = null; } else { if (!_factory.isProject(project)) { listener.getLogger().println("Detected unsupported subitem " + ModelHyperlinkNote.encodeTo(project) + ", skipping"); return; } origBranch = _factory.getBranch(project); if (!(origBranch instanceof Branch.Dead)) { if (!source.getId().equals(origBranch.getSourceId())) { // check who has priority int ourPriority = Integer.MAX_VALUE; int oldPriority = Integer.MAX_VALUE; int p = 1; for (BranchSource s : sources) { String sId = s.getSource().getId(); if (sId.equals(source.getId())) { ourPriority = p; } if (sId.equals(origBranch.getSourceId())) { oldPriority = p; } p++; } if (oldPriority < ourPriority) { listener.getLogger() .println("Ignoring " + ModelHyperlinkNote.encodeTo(project) + " from source #" + ourPriority + " as source #" + oldPriority + " owns the branch name"); return; } else { if (oldPriority == Integer.MAX_VALUE) { listener.getLogger() .println("Takeover for " + ModelHyperlinkNote.encodeTo(project) + " by source #" + ourPriority + " from source that no longer exists"); } else { listener.getLogger() .println("Takeover for " + ModelHyperlinkNote.encodeTo(project) + " by source #" + ourPriority + " from source #" + oldPriority); } } } } } Action[] revisionActions = new Action[0]; boolean headActionsFetched = false; try { branch.setActions(source.fetchActions(head, event, listener)); headActionsFetched = true; } catch (IOException | InterruptedException e) { printStackTrace(e, listener.error("Could not fetch metadata of branch %s", rawName)); } try { List<Action> actions = source.fetchActions(revision, event, listener); revisionActions = actions.toArray(new Action[actions.size()]); } catch (IOException | InterruptedException e) { printStackTrace(e, listener.error("Could not fetch metadata for revision %s of branch %s", revision, rawName)); } if (project != null) { boolean rebuild = (origBranch instanceof Branch.Dead && !(branch instanceof Branch.Dead)) || !(source.getId().equals(origBranch.getSourceId())); if (!headActionsFetched) { // we didn't fetch them so replicate previous actions branch.setActions(origBranch.getActions()); } boolean needSave = !branch.equals(origBranch) || !branch.getActions().equals(origBranch.getActions()) || !Util.getDigestOf(Items.XSTREAM2.toXML(branch.getScm())) .equals(Util.getDigestOf(Items.XSTREAM2.toXML(origBranch.getScm()))); _factory.decorate(_factory.setBranch(project, branch)); if (rebuild) { listener.getLogger().format("%s reopened: %s (%s)%n", StringUtils.defaultIfEmpty(head.getPronoun(), "Branch"), rawName, revision); needSave = true; if (isAutomaticBuild(source, head)) { scheduleBuild(_factory, project, revision, listener, rawName, causeFactory.create(source), revisionActions); } else { listener.getLogger().format("No automatic builds for %s%n", rawName); } } else if (revision.isDeterministic()) { SCMRevision lastBuild = _factory.getRevision(project); if (!revision.equals(lastBuild)) { listener.getLogger().format("Changes detected: %s (%s %s)%n", rawName, lastBuild, revision); needSave = true; if (isAutomaticBuild(source, head)) { scheduleBuild(_factory, project, revision, listener, rawName, causeFactory.create(source), revisionActions); } else { listener.getLogger().format("No automatic builds for %s%n", rawName); } } else { listener.getLogger().format("No changes detected: %s (still at %s)%n", rawName, revision); } } else { // fall back to polling when we have a non-deterministic revision/hash. SCMTriggerItem scmProject = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(project); if (scmProject != null) { PollingResult pollingResult = scmProject.poll(listener); if (pollingResult.hasChanges()) { listener.getLogger().format("Changes detected: %s%n", rawName); needSave = true; if (isAutomaticBuild(source, head)) { scheduleBuild(_factory, project, revision, listener, rawName, causeFactory.create(source), revisionActions); } else { listener.getLogger().format("No automatic builds for %s%n", rawName); } } else { listener.getLogger().format("No changes detected: %s%n", rawName); } } } try { if (needSave) { project.save(); } } catch (IOException e) { printStackTrace(e, listener.error("Could not save changes to " + rawName)); } return; } if (!observer.mayCreate(encodedName)) { listener.getLogger().println("Ignoring duplicate branch project " + rawName); return; } try (ChildNameGenerator.Trace trace = ChildNameGenerator.beforeCreateItem(MultiBranchProject.this, encodedName, branch.getName())) { if (getItem(encodedName) != null) { throw new IllegalStateException("JENKINS-42511: attempted to redundantly create " + encodedName + " in " + MultiBranchProject.this); } project = _factory.newInstance(branch); } if (!project.getName().equals(encodedName)) { throw new IllegalStateException( "Name of created project " + project + " did not match expected " + encodedName); } // HACK ALERT // ========== // We don't want to trigger a save, so we will do some trickery to ensure that observer.created(project) // performs the save BulkChange bc = new BulkChange(project); try { if (project.getDisplayNameOrNull() == null && !rawName.equals(encodedName)) { project.setDisplayName(rawName); } } catch (IOException e) { // ignore even if it does happen we didn't want a save } finally { bc.abort(); } // decorate contract is to ensure it dowes not trigger a save _factory.decorate(project); // ok it is now up to the observer to ensure it does the actual save. observer.created(project); if (isAutomaticBuild(source, head)) { scheduleBuild(_factory, project, revision, listener, rawName, causeFactory.create(source), revisionActions); } else { listener.getLogger().format("No automatic builds for %s%n", rawName); } } finally { observer.completed(encodedName); } } } /** * Tests if the specified {@link SCMHead} should be automatically built when discovered / modified. * @param source the source. * @param head the head. * @return {@code true} if the head should be automatically built when discovered / modified. */ private boolean isAutomaticBuild(SCMSource source, SCMHead head) { BranchSource branchSource = null; for (BranchSource s : sources) { if (s.getSource().getId().equals(source.getId())) { branchSource = s; break; } } if (branchSource == null) { // no match, means no build return false; } List<BranchBuildStrategy> buildStrategies = branchSource.getBuildStrategies(); if (buildStrategies.isEmpty()) { // we will use default behaviour, build anything but tags return !(head instanceof TagSCMHead); } else { for (BranchBuildStrategy s : buildStrategies) { if (s.isAutomaticBuild(source, head)) { return true; } } return false; } } /** * Adds the {@link MultiBranchProject.State#sourceActions} to {@link MultiBranchProject#getAllActions()}. * * @since 2.0 */ @Extension public static class StateActionFactory extends TransientActionFactory<MultiBranchProject> { /** * {@inheritDoc} */ @Override public Class<MultiBranchProject> type() { return MultiBranchProject.class; } /** * {@inheritDoc} */ @Nonnull @Override public Collection<? extends Action> createFor(@Nonnull MultiBranchProject target) { List<Action> result = new ArrayList<>(); MultiBranchProject<?, ?> project = target; for (BranchSource b : project.getSources()) { List<Action> actions = project.state.sourceActions.get(b.getSource().getId()); if (actions != null && !actions.isEmpty()) { result.addAll(actions); } } return result; } } /** * The persisted state. * * @since 2.0 */ private static class State implements Saveable { /** * Our owner. */ @NonNull private transient final MultiBranchProject<?, ?> owner; /** * The {@link SCMSource#fetchActions(SCMSourceEvent, TaskListener)} for each source, keyed by * {@link SCMSource#getId()}. */ private final Map<String, List<Action>> sourceActions = new HashMap<>(); /** * Constructor. * * @param owner our owner. */ private State(@NonNull MultiBranchProject<?, ?> owner) { this.owner = owner; } /** * Clear the state completely. */ public synchronized void reset() { sourceActions.clear(); } /** * Loads the state from disk. * * @throws IOException if there was an issue loading the state from disk. */ public synchronized void load() throws IOException { if (getStateFile().exists()) { getStateFile().unmarshal(this); } } /** * {@inheritDoc} */ @Override public void save() throws IOException { synchronized (this) { if (BulkChange.contains(this)) { return; } getStateFile().write(this); } SaveableListener.fireOnChange(this, getStateFile()); } /** * The file that the state is persisted in. * * @return The file that the state is persisted in. */ public final XmlFile getStateFile() { return new XmlFile(Items.XSTREAM, new File(owner.getRootDir(), "state.xml")); } } /** * Veto attempts to copy branch projects outside of their multibranch container. Only works on Jenkins core * versions with JENKINS-34691 merged. */ @Extension public static class CopyItemVeto extends ItemListener { //@Override // TODO once Jenkins 2.51+ which has JENKINS-34691 merged public void onCheckCopy(Item item, ItemGroup parent) throws Failure { if (item.getParent() instanceof MultiBranchProject) { throw new Failure(Messages.MultiBranchProject_CopyItemVeto_reason()); } } } // TODO pending method in Functions in 2.43+ static void printStackTrace(@CheckForNull Throwable t, @NonNull PrintWriter pw) { pw.println(Functions.printThrowable(t).trim()); } }