Skip to content

Commit

Permalink
Merge pull request #2 from ikedam/feature/JENKINS-24515_TargetFolder
Browse files Browse the repository at this point in the history
[JENKINS-24515] Folder support
  • Loading branch information
ikedam committed Sep 7, 2014
2 parents 992d25e + b52e0f2 commit f80becd
Show file tree
Hide file tree
Showing 5 changed files with 608 additions and 41 deletions.
16 changes: 15 additions & 1 deletion pom.xml
Expand Up @@ -3,7 +3,8 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>1.466</version><!-- which version of Jenkins is this plugin built against? -->
<!-- ModifiableTopLevelItemGroup is since 1.480.3 -->
<version>1.480.3</version><!-- which version of Jenkins is this plugin built against? -->
</parent>

<groupId>jp.ikedam.jenkins.plugins</groupId>
Expand Down Expand Up @@ -36,6 +37,19 @@
</licenses>

<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>cloudbees-folder</artifactId>
<version>4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<!--Depended by cloudbees-folder, to suppress annoying messages on tests-->
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
<version>1.8.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>promoted-builds</artifactId>
Expand Down
Expand Up @@ -23,17 +23,21 @@
*/
package jp.ikedam.jenkins.plugins.jobcopy_builder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;

import hudson.Extension;
import hudson.XmlFile;
import hudson.EnvVars;
import hudson.Launcher;
import hudson.DescriptorExtensionList;
import hudson.matrix.MatrixProject;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.TopLevelItem;
import hudson.model.BuildListener;
import hudson.model.Job;
import hudson.model.AbstractBuild;
import hudson.model.AbstractItem;
import hudson.model.AbstractProject;
Expand All @@ -42,17 +46,23 @@
import hudson.util.FormValidation;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import jenkins.model.ModifiableTopLevelItemGroup;
import jenkins.model.Jenkins;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

import com.google.common.base.Function;
import com.google.common.collect.Lists;

import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

/**
Expand Down Expand Up @@ -186,6 +196,7 @@ public JobcopyBuilder(String fromJobName, String toJobName, boolean overwrite, L
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
throws IOException, InterruptedException
{
ItemGroup<?> context = build.getProject().getRootProject().getParent();
EnvVars env = build.getEnvironment(listener);

if(StringUtils.isBlank(getFromJobName()))
Expand Down Expand Up @@ -217,21 +228,21 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
listener.getLogger().println(String.format("Copying %s to %s", fromJobNameExpanded, toJobNameExpanded));

// Reteive the job to be copied from.
TopLevelItem fromJob = Jenkins.getInstance().getItem(fromJobNameExpanded);
TopLevelItem fromJob = getRelative(fromJobNameExpanded, context, TopLevelItem.class);

if(fromJob == null)
{
listener.getLogger().println("Error: Item was not found.");
listener.getLogger().println(String.format("Error: Item '%s 'was not found.", fromJob));
return false;
}
else if(!(fromJob instanceof Job<?,?>))
else if(!(fromJob instanceof AbstractItem))
{
listener.getLogger().println("Error: Item was found, but is not a job.");
listener.getLogger().println(String.format("Error: Item '%s' was found, but cannot be copied (does not support AbstractItem).", fromJob));
return false;
}

// Check whether the job to be copied to is already exists.
TopLevelItem toJob = Jenkins.getInstance().getItem(toJobNameExpanded);
TopLevelItem toJob = getRelative(toJobNameExpanded, context, TopLevelItem.class);
if(toJob != null){
listener.getLogger().println(String.format("Already exists: %s", toJobNameExpanded));
if(!isOverwrite()){
Expand All @@ -245,10 +256,9 @@ else if(!(fromJob instanceof Job<?,?>))
}

// Retrieve the config.xml of the job copied from.
// TODO: what happens if this runs on a slave node?
listener.getLogger().println(String.format("Fetching configuration of %s...", fromJobNameExpanded));

XmlFile file = ((Job<?,?>)fromJob).getConfigFile();
XmlFile file = ((AbstractItem)fromJob).getConfigFile();
String jobConfigXmlString = file.asString();
String encoding = file.sniffEncoding();
listener.getLogger().println("Original xml:");
Expand All @@ -274,7 +284,28 @@ else if(!(fromJob instanceof Job<?,?>))
// Create the job copied to.
listener.getLogger().println(String.format("Creating %s", toJobNameExpanded));
InputStream is = new ByteArrayInputStream(jobConfigXmlString.getBytes(encoding));
toJob = Jenkins.getInstance().createProjectFromXML(toJobNameExpanded, is);
ItemGroup<?> toContext = context;
if(toJobNameExpanded.lastIndexOf('/') >= 0)
{
int pos = toJobNameExpanded.lastIndexOf('/');
String parentName = toJobNameExpanded.substring(0, pos);
toJobNameExpanded = toJobNameExpanded.substring(pos + 1);

toContext = getRelative(parentName, context, ItemGroup.class);
if(toContext == null)
{
listener.getLogger().println(String.format("Error: Target folder '%s' was not found.", parentName));
return false;
}
}

if(!(toContext instanceof ModifiableTopLevelItemGroup))
{
listener.getLogger().println(String.format("Error: Target folder '%s' does not support ModifiableTopLevelItemGroup", toContext.getFullName()));
return false;
}

toJob = ((ModifiableTopLevelItemGroup)toContext).createProjectFromXML(toJobNameExpanded, is);
if(toJob == null)
{
listener.getLogger().println(String.format("Failed to create %s", toJobNameExpanded));
Expand Down Expand Up @@ -302,7 +333,7 @@ else if(!(fromJob instanceof Job<?,?>))

try
{
target.updateByXml(new StreamSource(is));
target.updateByXml((Source)new StreamSource(is));
}
catch(IOException e)
{
Expand Down Expand Up @@ -331,7 +362,7 @@ else if(!(fromJob instanceof Job<?,?>))

// Do null update to reload the configuration.
AbstractItem target = (AbstractItem)toJob;
target.updateByXml(new StreamSource(target.getConfigFile().readRaw()));
target.updateByXml((Source)new StreamSource(target.getConfigFile().readRaw()));
}

// add the information of jobs copied from and to to the build.
Expand All @@ -340,6 +371,73 @@ else if(!(fromJob instanceof Job<?,?>))
return true;
}

/**
* Reimplementation of {@link Jenkins#getItem(String, ItemGroup, Class)}
*
* Existing implementation has following problems:
* * Falls back to {@link Jenkins#getItemByFullName(String)}
* * Cannot get {@link ItemGroup}
*
* @param pathName
* @param context
* @param klass
* @return
*/
public static <T> T getRelative(String pathName, ItemGroup<?> context, Class<T> klass)
{
if(context==null)
{
context = Jenkins.getInstance().getItemGroup();
}
if (pathName==null)
{
return null;
}

if (pathName.startsWith("/"))
{
// absolute
Item item = Jenkins.getInstance().getItemByFullName(pathName);
return klass.isInstance(item)?klass.cast(item):null;
}

Object/*Item|ItemGroup*/ ctx = context;

StringTokenizer tokens = new StringTokenizer(pathName,"/");
while(tokens.hasMoreTokens())
{
String s = tokens.nextToken();
if(s.equals(".."))
{
if(!(ctx instanceof Item))
{
// can't go up further
return null;
}
ctx = ((Item)ctx).getParent();
continue;
}
if(s.equals("."))
{
continue;
}

if(!(ctx instanceof ItemGroup))
{
return null;
}
ItemGroup<?> g = (ItemGroup<?>)ctx;
Item i = g.getItem(s);
if (i == null || !i.hasPermission(Item.READ))
{
return null;
}
ctx=i;
}

return klass.isInstance(ctx)?klass.cast(ctx):null;
}

/**
* The internal class to work with views.
*
Expand Down Expand Up @@ -399,11 +497,23 @@ public DescriptorExtensionList<JobcopyOperation,Descriptor<JobcopyOperation>> ge
*
* Used for the autocomplete of From Job Name.
*
* @return the list of jobs
* @return the list of names of jobs
*/
public ComboBoxModel doFillFromJobNameItems()
public ComboBoxModel doFillFromJobNameItems(@AncestorInPath AbstractProject<?,?> project)
{
return new ComboBoxModel(Jenkins.getInstance().getTopLevelItemNames());
final ItemGroup<?> context = (project != null)?project.getParent():Jenkins.getInstance().getItemGroup();
List<String> itemList = new ArrayList<String>(Lists.transform(
Jenkins.getInstance().getAllItems(AbstractItem.class),
new Function<Item, String>()
{
public String apply(Item input)
{
return input.getRelativeNameFrom(context);
}
}
));
Collections.sort(itemList);
return new ComboBoxModel(itemList);
}

/**
Expand Down Expand Up @@ -476,8 +586,9 @@ private boolean containsVariable(String value)
* @param warnIfNotExists
* @return
*/
public FormValidation doCheckJobName(String jobName, boolean warnIfExists, boolean warnIfNotExists)
public FormValidation doCheckJobName(AbstractProject<?,?> project, String jobName, boolean warnIfExists, boolean warnIfNotExists)
{
ItemGroup<?> context = (project != null)?project.getParent():Jenkins.getInstance().getItemGroup();
jobName = StringUtils.trim(jobName);

if(StringUtils.isBlank(jobName))
Expand All @@ -489,14 +600,18 @@ public FormValidation doCheckJobName(String jobName, boolean warnIfExists, boole
return FormValidation.ok();
}

TopLevelItem job = Jenkins.getInstance().getItem(jobName);
TopLevelItem job = getRelative(jobName, context, TopLevelItem.class);
if(job != null)
{
// job exists
if(warnIfExists)
{
return FormValidation.warning(Messages.JobCopyBuilder_JobName_exists());
}
if(!(job instanceof AbstractItem))
{
return FormValidation.warning(Messages.JobCopyBuilder_JobName_notAbstractItem());
}
}
else
{
Expand All @@ -516,9 +631,9 @@ public FormValidation doCheckJobName(String jobName, boolean warnIfExists, boole
* @param fromJobName
* @return
*/
public FormValidation doCheckFromJobName(@QueryParameter String fromJobName)
public FormValidation doCheckFromJobName(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String fromJobName)
{
return doCheckJobName(fromJobName, false, true);
return doCheckJobName(project, fromJobName, false, true);
}

/**
Expand All @@ -528,9 +643,9 @@ public FormValidation doCheckFromJobName(@QueryParameter String fromJobName)
* @param overwrite
* @return FormValidation object.
*/
public FormValidation doCheckToJobName(@QueryParameter String toJobName, @QueryParameter boolean overwrite)
public FormValidation doCheckToJobName(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String toJobName, @QueryParameter boolean overwrite)
{
return doCheckJobName(toJobName, !overwrite, false);
return doCheckJobName(project, toJobName, !overwrite, false);
}
}
}
Expand Down
Expand Up @@ -28,6 +28,7 @@ CopiedjobinfoAction.DisplayName=Copied Job
JobCopyBuilder.JobName.empty=Cannot be blank.
JobCopyBuilder.JobName.exists=Specified job already exists. Check "Overwrite", or a build will fail if the job exists at the execution time.
JobCopyBuilder.JobName.notExists=Spedivied job does not exist. A build will fail if the job does not exist at the execution time.
JobCopyBuilder.JobName.notAbstractItem=Specified item cannot be copied (does not implement AbstractItem).
ReplaceOperation.fromStr.empty=Cannot be empty. Empty string results in a failure of the build.
ReplaceOperation.fromStr.enclosedWithBlank=String is surrounded with white spaces. Replacement is performed without trimming white spaces, the replacement may not work as you expects.
AdditionalFileSet.includeFile.empty=Cannot be blank.
Expand Up @@ -36,6 +36,8 @@ JobCopyBuilder.JobName.empty=\u5024\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3
JobCopyBuilder.JobName.exists=\u6307\u5b9a\u306e\u30b8\u30e7\u30d6\u306f\u65e2\u306b\u5b58\u5728\u3057\u3066\u3044\u307e\u3059\u3002\u300c\u4e0a\u66f8\u304d\u3059\u308b\u300d\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u306a\u3044\u5834\u5408\u3001\u5b9f\u884c\u6642\u306b\u30b3\u30d4\u30fc\u5148\u306e\u30b8\u30e7\u30d6\u304c\u5b58\u5728\u3059\u308b\u3068\u30d3\u30eb\u30c9\u306b\u5931\u6557\u3057\u307e\u3059\u3002
# JobCopyBuilder.JobName.notExists=指定のジョブは存在しません。実行時にコピー元のジョブが存在しない場合、ビルドに失敗します。
JobCopyBuilder.JobName.notExists=\u6307\u5b9a\u306e\u30b8\u30e7\u30d6\u306f\u5b58\u5728\u3057\u307e\u305b\u3093\u3002\u5b9f\u884c\u6642\u306b\u30b3\u30d4\u30fc\u5143\u306e\u30b8\u30e7\u30d6\u304c\u5b58\u5728\u3057\u306a\u3044\u5834\u5408\u3001\u30d3\u30eb\u30c9\u306b\u5931\u6557\u3057\u307e\u3059\u3002
# JobCopyBuilder.JobName.notAbstractItem=指定されたアイテムはコピーできません (AbstractItem を実装していない)。
JobCopyBuilder.JobName.notAbstractItem=\u6307\u5b9a\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u306f\u30b3\u30d4\u30fc\u3067\u304d\u307e\u305b\u3093 (AbstractItem \u3092\u5b9f\u88c5\u3057\u3066\u3044\u306a\u3044)\u3002
# ReplaceOperation.fromStr.empty=値を指定してください。置き換え前の文字列が空文字列の場合、ビルドに失敗します。
ReplaceOperation.fromStr.empty=\u5024\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u7f6e\u304d\u63db\u3048\u524d\u306e\u6587\u5b57\u5217\u304c\u7a7a\u6587\u5b57\u5217\u306e\u5834\u5408\u3001\u30d3\u30eb\u30c9\u306b\u5931\u6557\u3057\u307e\u3059\u3002
# ReplaceOperation.fromStr.enclosedWithBlank=文字列の前後に空白文字があります。前後の空白文字を含んで置き換え処理を行うので、意図した通りの動作をしない場合があります。
Expand Down

0 comments on commit f80becd

Please sign in to comment.