/*
* The MIT License
*
* Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, id:cactusman, Timothy Bingaman
*
* 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.ivy;
import hudson.CopyOnWrite;
import hudson.Functions;
import hudson.Util;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.DependencyGraph;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.JDK;
import hudson.model.Job;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.Resource;
import hudson.model.Result;
import hudson.model.Saveable;
import hudson.model.DependencyGraph.Dependency;
import hudson.model.Descriptor.FormException;
import hudson.model.queue.CauseOfBlockage;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildWrapper;
import hudson.tasks.LogRotator;
import hudson.tasks.Publisher;
import hudson.util.DescribableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import org.apache.commons.lang.StringUtils;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
/**
* {@link Job} that builds projects based on Ivy.
*
* @author Timothy Bingaman
*/
public final class IvyModule extends AbstractIvyProject<IvyModule, IvyBuild> implements Saveable {
private static final String IVY_XML_PATH = "ivy.xml";
private DescribableList<Publisher, Descriptor<Publisher>> publishers = new DescribableList<Publisher, Descriptor<Publisher>>(this);
/**
* Name taken from {@link ModuleRevisionId#getName()}.
*/
private String displayName;
/**
* Revision number of this module as of the last build, taken from
* {@link ModuleRevisionId#getRevision()}.
*
* This field can be null if Hudson loaded old data that didn't record this
* information, so that situation needs to be handled gracefully.
*/
private String revision;
/**
* Ivy branch of this module as of the last build, taken from
* {@link ModuleRevisionId#getBranch()}.
*
* This field can be null if Hudson loaded old data that didn't record this
* information, so that situation needs to be handled gracefully.
*/
private String ivyBranch;
private transient ModuleName moduleName;
/**
* Relative path from the workspace to the ivy descriptor file for this
* module.
*
* Strings like "ivy.xml" (if the ivy.xml file is checked out directly in
* the workspace), "abc/ivy.xml", "foo/bar/zot/ivy.xml".
*/
private String relativePathToDescriptorFromWorkspace;
/**
* If this module has targets specified by itself. Otherwise leave it null
* to use the default targets specified in the parent.
*/
private String targets;
/**
* Relative path from the workspace to the ivy descriptor file for this
* module.
*
* Strings like "ivy.xml" (if the ivy.xml file is directly in
* the module root), "ivy/ivy.xml", "build/ivy.xml".
*/
private String relativePathToDescriptorFromModuleRoot;
private DescribableList<BuildWrapper,Descriptor<BuildWrapper>> buildWrappers =
new DescribableList<BuildWrapper, Descriptor<BuildWrapper>>(this);
public DescribableList<BuildWrapper, Descriptor<BuildWrapper>> getBuildWrappersList() {
if(buildWrappers == null)
{
buildWrappers =
new DescribableList<BuildWrapper, Descriptor<BuildWrapper>>(this);
}
return buildWrappers;
}
/**
* List of modules that this module declares direct dependencies on.
*/
@CopyOnWrite
private volatile Set<ModuleDependency> dependencies;
/* package */IvyModule(IvyModuleSet parent, IvyModuleInfo moduleInfo, int firstBuildNumber) throws IOException {
super(parent, moduleInfo.name.toFileSystemName());
reconfigure(moduleInfo);
updateNextBuildNumber(firstBuildNumber);
copyParentBuildWrappers(parent);
}
private void copyParentBuildWrappers(IvyModuleSet parent)
{
if(!parent.isAggregatorStyleBuild())
{
List<BuildWrapper> parentWrappers = parent.getBuildWrappersList().getAll(BuildWrapper.class);
for (BuildWrapper buildWrapper : parentWrappers) {
try
{
IvyClonerWrapper cloner = new IvyClonerWrapper();
cloner.dontClone(Descriptor.class);
getBuildWrappersList().add(cloner.deepClone(buildWrapper));
}
catch(IOException e)
{
throw new RuntimeException("Could not copy build wrappers", e);
}
}
}
}
/**
* {@link IvyModule} follows the same log rotation schedule as its parent.
*/
@Override
public LogRotator getLogRotator() {
return getParent().getLogRotator();
}
/**
* @deprecated Not allowed to configure log rotation per module.
*/
@Deprecated
@Override
public void setLogRotator(LogRotator logRotator) {
throw new UnsupportedOperationException();
}
@Override
public boolean supportsLogRotator() {
return false;
}
@Override
public boolean isBuildable() {
// not buildable if the parent project is disabled
return super.isBuildable() && getParent().isBuildable();
}
/**
* Called to update the module with the new ivy.xml information.
* <p>
* This method is invoked on {@link IvyModule} that has the matching
* {@link ModuleName}.
*/
/* package */final void reconfigure(IvyModuleInfo moduleInfo) {
this.displayName = moduleInfo.displayName;
this.revision = moduleInfo.revision;
this.ivyBranch = moduleInfo.branch;
this.relativePathToDescriptorFromWorkspace = moduleInfo.relativePathToDescriptor;
this.dependencies = moduleInfo.dependencies;
disabled = false;
}
@Override
protected void doSetName(String name) {
moduleName = ModuleName.fromFileSystemName(name);
super.doSetName(moduleName.toString());
}
@Override
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
super.onLoad(parent, name);
if (publishers == null)
publishers = new DescribableList<Publisher, Descriptor<Publisher>>(this);
publishers.setOwner(this);
if (dependencies == null) {
dependencies = Collections.emptySet();
}
}
/**
* Relative path to this module's root directory from the workspace of a
* {@link IvyModuleSet}.
*
* The path separator is normalized to '/'.
*/
public String getRelativePath() {
return relativePathToDescriptorFromWorkspace;
}
/**
* Gets the revision number in the ivy.xml file as of the last build.
*
* @return This method can return null if Hudson loaded old data that didn't
* record this information, so that situation needs to be handled
* gracefully.
*/
public String getRevision() {
return revision;
}
/**
* Gets the Ivy branch in the ivy.xml file as of the last build.
*
* @return This method can return null if Hudson loaded old data that didn't
* record this information, so that situation needs to be handled
* gracefully.
*/
public String getIvyBranch() {
return ivyBranch;
}
/**
* Gets the list of targets to execute for this module.
*/
public String getTargets() {
return targets;
}
public String getRelativePathToDescriptorFromModuleRoot() {
if (relativePathToDescriptorFromModuleRoot != null)
return relativePathToDescriptorFromModuleRoot;
return getParent().getRelativePathToDescriptorFromModuleRoot();
}
public String getUserConfiguredRelativePathToDescriptorFromModuleRoot() {
return relativePathToDescriptorFromModuleRoot;
}
public String getRelativePathToModuleRoot() {
return StringUtils.removeEnd(relativePathToDescriptorFromWorkspace, StringUtils.defaultString(getRelativePathToDescriptorFromModuleRoot(),
IVY_XML_PATH));
}
@Override
public DescribableList<Publisher, Descriptor<Publisher>> getPublishersList() {
if (getParent().isAggregatorStyleBuild()) {
return publishers;
}
DescribableList<Publisher, Descriptor<Publisher>> publishersList = new DescribableList<Publisher, Descriptor<Publisher>>(Saveable.NOOP);
try {
publishersList.addAll(createModulePublishers());
} catch (IOException e) {
LOGGER.warning("Failed to load module publisher list");
}
return publishersList;
}
@Override
public JDK getJDK() {
// share one setting for the whole module set.
return getParent().getJDK();
}
@Override
protected Class<IvyBuild> getBuildClass() {
return IvyBuild.class;
}
@Override
protected IvyBuild newBuild() throws IOException {
return super.newBuild();
}
public ModuleName getModuleName() {
return moduleName;
}
/**
* Gets organisation+name+revision as {@link ModuleDependency}.
*/
public ModuleDependency asDependency() {
return new ModuleDependency(moduleName, Functions.defaulted(revision, ModuleDependency.UNKNOWN), Functions.defaulted(ivyBranch,
ModuleDependency.UNKNOWN));
}
@Override
public String getShortUrl() {
return moduleName.toFileSystemName() + '/';
}
@Exported(visibility = 2)
@Override
public String getDisplayName() {
return displayName;
}
@Override
public String getPronoun() {
return Messages.IvyModule_Pronoun();
}
@Override
public boolean isNameEditable() {
return false;
}
@Override
public IvyModuleSet getParent() {
return (IvyModuleSet) super.getParent();
}
/**
* {@link IvyModule} uses the workspace of the {@link IvyModuleSet}, so it
* always needs to be built on the same slave as the parent.
*/
@Override
public Label getAssignedLabel() {
Node n = getParent().getLastBuiltOn();
if (n == null)
return null;
return n.getSelfLabel();
}
/**
* Workspace of a {@link IvyModule} is a part of the parent's workspace.
* <p>
* That is, {@Link IvyModuleSet} builds are incompatible with any
* {@link IvyModule} builds, whereas {@link IvyModule} builds are compatible
* with each other.
*
* @deprecated as of 1.319 in {@link AbstractProject}.
*/
@Deprecated
@Override
public Resource getWorkspaceResource() {
return new Resource(getParent().getWorkspaceResource(), getDisplayName() + " workspace");
}
@Override
public boolean isFingerprintConfigured() {
return true;
}
@Override // to make this accessible to IvyModuleSet
protected void updateTransientActions() {
super.updateTransientActions();
}
@Override
protected void buildDependencyGraph(DependencyGraph graph) {
// Allow a module's publishers to add to the dependency graph.
// This permits the standard build trigger to still work in
// addition to the triggers generated by ivy dependencies.
publishers.buildDependencyGraph(this,graph);
if (!isBuildable() || (getParent().ignoreUpstreamChanges() && getParent().isAggregatorStyleBuild()))
return;
IvyDependencyComputationData data = graph.getComputationalData(IvyDependencyComputationData.class);
// Build a map of all Ivy modules in this Jenkins instance as dependencies.
if (!getParent().ignoreUpstreamChanges() && data == null) {
Map<ModuleDependency, IvyModule> modules = new HashMap<ModuleDependency, IvyModule>();
for (IvyModule m : getAllIvyModules()) {
if(!m.isBuildable() || !m.getParent().isAllowedToTriggerDownstream()) continue;
ModuleDependency moduleDependency = m.asDependency();
modules.put(moduleDependency, m);
modules.put(moduleDependency.withUnknownRevision(), m);
}
data = new IvyDependencyComputationData(modules);
graph.putComputationalData(IvyDependencyComputationData.class, data);
}
// In case two modules with the same name are defined, modules in the same IvyModuleSet
// take precedence.
Map<ModuleDependency, IvyModule> myParentsModules = new HashMap<ModuleDependency, IvyModule>();
for (IvyModule m : getParent().getModules()) {
if(m.isDisabled()) continue;
ModuleDependency moduleDependency = m.asDependency();
myParentsModules.put(moduleDependency,m);
myParentsModules.put(moduleDependency.withUnknownRevision(),m);
}
// if the build style is the aggregator build, define dependencies against project,
// not module.
AbstractProject<?, ?> downstream = getParent().isAggregatorStyleBuild() ? getParent() : this;
for (ModuleDependency d : dependencies) {
IvyModule src = myParentsModules.get(d);
if (src == null) {
src = myParentsModules.get(d.withUnknownRevision());
}
if(src == null && !getParent().ignoreUpstreamChanges()) {
src = data.allModules.get(d);
if (src == null) {
src = data.allModules.get(d.withUnknownRevision());
}
}
if (src == null)
continue;
AbstractProject upstream;
if (src.getParent().isAggregatorStyleBuild()) {
upstream = src.getParent();
} else {
// Add a virtual dependency from the parent project to the
// downstream one to make the
// "Block build when upstream project is building" option behave
// properly
if (!this.getParent().equals(src.getParent()) && !hasDependency(graph, src.getParent(), downstream))
graph.addDependency(new IvyVirtualDependency(src.getParent(), downstream));
upstream = src;
}
AbstractProject revisedDownstream = downstream;
if(!getParent().equals(src.getParent()) && !getParent().isAggregatorStyleBuild())
{
revisedDownstream = getParent();
if (!src.getParent().isAggregatorStyleBuild() && !hasDependency(graph, src.getParent(),revisedDownstream))
{
graph.addDependency(new IvyVirtualDependency(src.getParent(), revisedDownstream));
}
}
// Create the build dependency, ignoring self-referencing or already existing deps
if (upstream != revisedDownstream && !hasDependency(graph, upstream, revisedDownstream))
graph.addDependency(new IvyThresholdDependency(upstream, revisedDownstream, Result.SUCCESS, isUseUpstreamParameters()));
}
}
/**
* Returns all Ivy modules in this Jenkins instance.
*/
protected Collection<IvyModule> getAllIvyModules() {
return Hudson.getInstance().getAllItems(IvyModule.class);
}
private boolean hasDependency(DependencyGraph graph, AbstractProject upstream, AbstractProject downstream) {
for (Dependency dep : graph.getDownstreamDependencies(upstream)) {
if (dep instanceof IvyDependency && dep.getDownstreamProject().equals(downstream))
return true;
}
return false;
}
private static class IvyDependencyComputationData {
Map<ModuleDependency, IvyModule> allModules;
public IvyDependencyComputationData(Map<ModuleDependency, IvyModule> modules) {
this.allModules = modules;
}
}
@Override
public CauseOfBlockage getCauseOfBlockage() {
CauseOfBlockage cob = super.getCauseOfBlockage();
if (cob != null)
return cob;
if (!getParent().isAggregatorStyleBuild()) {
DependencyGraph graph = Hudson.getInstance().getDependencyGraph();
for (AbstractProject tup : graph.getTransitiveUpstream(this)) {
if(getParent() == tup.getParent() && (tup.isBuilding() || tup.isInQueue()))
return new BecauseOfUpstreamModuleBuildInProgress(tup);
}
}
return null;
}
/**
* Because the upstream module build is in progress, and we are configured to wait for that.
*/
public static class BecauseOfUpstreamModuleBuildInProgress extends CauseOfBlockage {
public final AbstractProject<?,?> up;
public BecauseOfUpstreamModuleBuildInProgress(AbstractProject<?,?> up) {
this.up = up;
}
@Override
public String getShortDescription() {
return Messages.IvyModule_UpstreamModuleBuildInProgress(up.getName());
}
}
@Override
protected void addTransientActionsFromBuild(IvyBuild build, List<Action> collection, Set<Class> added) {
if (build == null)
return;
List<IvyReporter> list = build.projectActionReporters;
if (list == null)
return;
for (IvyReporter step : list) {
if (!added.add(step.getClass()))
continue; // already added
try {
collection.addAll(step.getProjectActions(this));
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to getProjectAction from " + step + ". Report issue to plugin developers.", e);
}
}
}
/**
* List of active {@link Publisher}s configured for this module.
*/
public DescribableList<Publisher, Descriptor<Publisher>> getPublishers() {
return publishers;
}
@Override
protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
super.submit(req, rsp);
targets = Util.fixEmptyAndTrim(req.getParameter("targets"));
relativePathToDescriptorFromModuleRoot = Util.fixEmptyAndTrim(req.getParameter("relativePathToDescriptorFromModuleRoot"));
publishers.rebuild(req,req.getSubmittedForm(),BuildStepDescriptor.filter(Publisher.all(),this.getClass()));
// dependency setting might have been changed by the user, so rebuild.
Hudson.getInstance().rebuildDependencyGraph();
}
@Override
protected void performDelete() throws IOException, InterruptedException {
super.performDelete();
getParent().onModuleDeleted(this);
}
/**
* Creates a list of {@link Publisher}s to be used for a build of this project.
*/
protected final List<Publisher> createModulePublishers() {
List<Publisher> modulePublisherList = new ArrayList<Publisher>();
getPublishers().addAllTo(modulePublisherList);
if (!getParent().isAggregatorStyleBuild()) {
getParent().getPublishers().addAllTo(modulePublisherList);
}
return modulePublisherList;
}
@Override
public boolean isUseUpstreamParameters() {
return getParent().isUseUpstreamParameters();
}
private static final Logger LOGGER = Logger.getLogger(IvyModule.class.getName());
}