Skip to content

Commit f80becd

Browse files
committedSep 7, 2014
Merge pull request #2 from ikedam/feature/JENKINS-24515_TargetFolder
[JENKINS-24515] Folder support
2 parents 992d25e + b52e0f2 commit f80becd

File tree

5 files changed

+608
-41
lines changed

5 files changed

+608
-41
lines changed
 

‎pom.xml

+15-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<parent>
44
<groupId>org.jenkins-ci.plugins</groupId>
55
<artifactId>plugin</artifactId>
6-
<version>1.466</version><!-- which version of Jenkins is this plugin built against? -->
6+
<!-- ModifiableTopLevelItemGroup is since 1.480.3 -->
7+
<version>1.480.3</version><!-- which version of Jenkins is this plugin built against? -->
78
</parent>
89

910
<groupId>jp.ikedam.jenkins.plugins</groupId>
@@ -36,6 +37,19 @@
3637
</licenses>
3738

3839
<dependencies>
40+
<dependency>
41+
<groupId>org.jenkins-ci.plugins</groupId>
42+
<artifactId>cloudbees-folder</artifactId>
43+
<version>4.0</version>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<!--Depended by cloudbees-folder, to suppress annoying messages on tests-->
48+
<groupId>org.jenkins-ci.plugins</groupId>
49+
<artifactId>credentials</artifactId>
50+
<version>1.8.3</version>
51+
<scope>test</scope>
52+
</dependency>
3953
<dependency>
4054
<groupId>org.jenkins-ci.plugins</groupId>
4155
<artifactId>promoted-builds</artifactId>

‎src/main/java/jp/ikedam/jenkins/plugins/jobcopy_builder/JobcopyBuilder.java

+135-20
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,21 @@
2323
*/
2424
package jp.ikedam.jenkins.plugins.jobcopy_builder;
2525

26+
import java.util.ArrayList;
27+
import java.util.Collections;
2628
import java.util.List;
29+
import java.util.StringTokenizer;
2730

2831
import hudson.Extension;
2932
import hudson.XmlFile;
3033
import hudson.EnvVars;
3134
import hudson.Launcher;
3235
import hudson.DescriptorExtensionList;
3336
import hudson.matrix.MatrixProject;
37+
import hudson.model.Item;
38+
import hudson.model.ItemGroup;
3439
import hudson.model.TopLevelItem;
3540
import hudson.model.BuildListener;
36-
import hudson.model.Job;
3741
import hudson.model.AbstractBuild;
3842
import hudson.model.AbstractItem;
3943
import hudson.model.AbstractProject;
@@ -42,17 +46,23 @@
4246
import hudson.util.FormValidation;
4347
import hudson.tasks.Builder;
4448
import hudson.tasks.BuildStepDescriptor;
49+
import jenkins.model.ModifiableTopLevelItemGroup;
4550
import jenkins.model.Jenkins;
4651

4752
import org.apache.commons.lang.StringUtils;
53+
import org.kohsuke.stapler.AncestorInPath;
4854
import org.kohsuke.stapler.DataBoundConstructor;
4955
import org.kohsuke.stapler.QueryParameter;
5056

57+
import com.google.common.base.Function;
58+
import com.google.common.collect.Lists;
59+
5160
import java.io.InputStream;
5261
import java.io.ByteArrayInputStream;
5362
import java.io.IOException;
5463
import java.io.Serializable;
5564

65+
import javax.xml.transform.Source;
5666
import javax.xml.transform.stream.StreamSource;
5767

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

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

219230
// Reteive the job to be copied from.
220-
TopLevelItem fromJob = Jenkins.getInstance().getItem(fromJobNameExpanded);
231+
TopLevelItem fromJob = getRelative(fromJobNameExpanded, context, TopLevelItem.class);
221232

222233
if(fromJob == null)
223234
{
224-
listener.getLogger().println("Error: Item was not found.");
235+
listener.getLogger().println(String.format("Error: Item '%s 'was not found.", fromJob));
225236
return false;
226237
}
227-
else if(!(fromJob instanceof Job<?,?>))
238+
else if(!(fromJob instanceof AbstractItem))
228239
{
229-
listener.getLogger().println("Error: Item was found, but is not a job.");
240+
listener.getLogger().println(String.format("Error: Item '%s' was found, but cannot be copied (does not support AbstractItem).", fromJob));
230241
return false;
231242
}
232243

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

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

251-
XmlFile file = ((Job<?,?>)fromJob).getConfigFile();
261+
XmlFile file = ((AbstractItem)fromJob).getConfigFile();
252262
String jobConfigXmlString = file.asString();
253263
String encoding = file.sniffEncoding();
254264
listener.getLogger().println("Original xml:");
@@ -274,7 +284,28 @@ else if(!(fromJob instanceof Job<?,?>))
274284
// Create the job copied to.
275285
listener.getLogger().println(String.format("Creating %s", toJobNameExpanded));
276286
InputStream is = new ByteArrayInputStream(jobConfigXmlString.getBytes(encoding));
277-
toJob = Jenkins.getInstance().createProjectFromXML(toJobNameExpanded, is);
287+
ItemGroup<?> toContext = context;
288+
if(toJobNameExpanded.lastIndexOf('/') >= 0)
289+
{
290+
int pos = toJobNameExpanded.lastIndexOf('/');
291+
String parentName = toJobNameExpanded.substring(0, pos);
292+
toJobNameExpanded = toJobNameExpanded.substring(pos + 1);
293+
294+
toContext = getRelative(parentName, context, ItemGroup.class);
295+
if(toContext == null)
296+
{
297+
listener.getLogger().println(String.format("Error: Target folder '%s' was not found.", parentName));
298+
return false;
299+
}
300+
}
301+
302+
if(!(toContext instanceof ModifiableTopLevelItemGroup))
303+
{
304+
listener.getLogger().println(String.format("Error: Target folder '%s' does not support ModifiableTopLevelItemGroup", toContext.getFullName()));
305+
return false;
306+
}
307+
308+
toJob = ((ModifiableTopLevelItemGroup)toContext).createProjectFromXML(toJobNameExpanded, is);
278309
if(toJob == null)
279310
{
280311
listener.getLogger().println(String.format("Failed to create %s", toJobNameExpanded));
@@ -302,7 +333,7 @@ else if(!(fromJob instanceof Job<?,?>))
302333

303334
try
304335
{
305-
target.updateByXml(new StreamSource(is));
336+
target.updateByXml((Source)new StreamSource(is));
306337
}
307338
catch(IOException e)
308339
{
@@ -331,7 +362,7 @@ else if(!(fromJob instanceof Job<?,?>))
331362

332363
// Do null update to reload the configuration.
333364
AbstractItem target = (AbstractItem)toJob;
334-
target.updateByXml(new StreamSource(target.getConfigFile().readRaw()));
365+
target.updateByXml((Source)new StreamSource(target.getConfigFile().readRaw()));
335366
}
336367

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

374+
/**
375+
* Reimplementation of {@link Jenkins#getItem(String, ItemGroup, Class)}
376+
*
377+
* Existing implementation has following problems:
378+
* * Falls back to {@link Jenkins#getItemByFullName(String)}
379+
* * Cannot get {@link ItemGroup}
380+
*
381+
* @param pathName
382+
* @param context
383+
* @param klass
384+
* @return
385+
*/
386+
public static <T> T getRelative(String pathName, ItemGroup<?> context, Class<T> klass)
387+
{
388+
if(context==null)
389+
{
390+
context = Jenkins.getInstance().getItemGroup();
391+
}
392+
if (pathName==null)
393+
{
394+
return null;
395+
}
396+
397+
if (pathName.startsWith("/"))
398+
{
399+
// absolute
400+
Item item = Jenkins.getInstance().getItemByFullName(pathName);
401+
return klass.isInstance(item)?klass.cast(item):null;
402+
}
403+
404+
Object/*Item|ItemGroup*/ ctx = context;
405+
406+
StringTokenizer tokens = new StringTokenizer(pathName,"/");
407+
while(tokens.hasMoreTokens())
408+
{
409+
String s = tokens.nextToken();
410+
if(s.equals(".."))
411+
{
412+
if(!(ctx instanceof Item))
413+
{
414+
// can't go up further
415+
return null;
416+
}
417+
ctx = ((Item)ctx).getParent();
418+
continue;
419+
}
420+
if(s.equals("."))
421+
{
422+
continue;
423+
}
424+
425+
if(!(ctx instanceof ItemGroup))
426+
{
427+
return null;
428+
}
429+
ItemGroup<?> g = (ItemGroup<?>)ctx;
430+
Item i = g.getItem(s);
431+
if (i == null || !i.hasPermission(Item.READ))
432+
{
433+
return null;
434+
}
435+
ctx=i;
436+
}
437+
438+
return klass.isInstance(ctx)?klass.cast(ctx):null;
439+
}
440+
343441
/**
344442
* The internal class to work with views.
345443
*
@@ -399,11 +497,23 @@ public DescriptorExtensionList<JobcopyOperation,Descriptor<JobcopyOperation>> ge
399497
*
400498
* Used for the autocomplete of From Job Name.
401499
*
402-
* @return the list of jobs
500+
* @return the list of names of jobs
403501
*/
404-
public ComboBoxModel doFillFromJobNameItems()
502+
public ComboBoxModel doFillFromJobNameItems(@AncestorInPath AbstractProject<?,?> project)
405503
{
406-
return new ComboBoxModel(Jenkins.getInstance().getTopLevelItemNames());
504+
final ItemGroup<?> context = (project != null)?project.getParent():Jenkins.getInstance().getItemGroup();
505+
List<String> itemList = new ArrayList<String>(Lists.transform(
506+
Jenkins.getInstance().getAllItems(AbstractItem.class),
507+
new Function<Item, String>()
508+
{
509+
public String apply(Item input)
510+
{
511+
return input.getRelativeNameFrom(context);
512+
}
513+
}
514+
));
515+
Collections.sort(itemList);
516+
return new ComboBoxModel(itemList);
407517
}
408518

409519
/**
@@ -476,8 +586,9 @@ private boolean containsVariable(String value)
476586
* @param warnIfNotExists
477587
* @return
478588
*/
479-
public FormValidation doCheckJobName(String jobName, boolean warnIfExists, boolean warnIfNotExists)
589+
public FormValidation doCheckJobName(AbstractProject<?,?> project, String jobName, boolean warnIfExists, boolean warnIfNotExists)
480590
{
591+
ItemGroup<?> context = (project != null)?project.getParent():Jenkins.getInstance().getItemGroup();
481592
jobName = StringUtils.trim(jobName);
482593

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

492-
TopLevelItem job = Jenkins.getInstance().getItem(jobName);
603+
TopLevelItem job = getRelative(jobName, context, TopLevelItem.class);
493604
if(job != null)
494605
{
495606
// job exists
496607
if(warnIfExists)
497608
{
498609
return FormValidation.warning(Messages.JobCopyBuilder_JobName_exists());
499610
}
611+
if(!(job instanceof AbstractItem))
612+
{
613+
return FormValidation.warning(Messages.JobCopyBuilder_JobName_notAbstractItem());
614+
}
500615
}
501616
else
502617
{
@@ -516,9 +631,9 @@ public FormValidation doCheckJobName(String jobName, boolean warnIfExists, boole
516631
* @param fromJobName
517632
* @return
518633
*/
519-
public FormValidation doCheckFromJobName(@QueryParameter String fromJobName)
634+
public FormValidation doCheckFromJobName(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String fromJobName)
520635
{
521-
return doCheckJobName(fromJobName, false, true);
636+
return doCheckJobName(project, fromJobName, false, true);
522637
}
523638

524639
/**
@@ -528,9 +643,9 @@ public FormValidation doCheckFromJobName(@QueryParameter String fromJobName)
528643
* @param overwrite
529644
* @return FormValidation object.
530645
*/
531-
public FormValidation doCheckToJobName(@QueryParameter String toJobName, @QueryParameter boolean overwrite)
646+
public FormValidation doCheckToJobName(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String toJobName, @QueryParameter boolean overwrite)
532647
{
533-
return doCheckJobName(toJobName, !overwrite, false);
648+
return doCheckJobName(project, toJobName, !overwrite, false);
534649
}
535650
}
536651
}

‎src/main/resources/jp/ikedam/jenkins/plugins/jobcopy_builder/Messages.properties

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ CopiedjobinfoAction.DisplayName=Copied Job
2828
JobCopyBuilder.JobName.empty=Cannot be blank.
2929
JobCopyBuilder.JobName.exists=Specified job already exists. Check "Overwrite", or a build will fail if the job exists at the execution time.
3030
JobCopyBuilder.JobName.notExists=Spedivied job does not exist. A build will fail if the job does not exist at the execution time.
31+
JobCopyBuilder.JobName.notAbstractItem=Specified item cannot be copied (does not implement AbstractItem).
3132
ReplaceOperation.fromStr.empty=Cannot be empty. Empty string results in a failure of the build.
3233
ReplaceOperation.fromStr.enclosedWithBlank=String is surrounded with white spaces. Replacement is performed without trimming white spaces, the replacement may not work as you expects.
3334
AdditionalFileSet.includeFile.empty=Cannot be blank.

‎src/main/resources/jp/ikedam/jenkins/plugins/jobcopy_builder/Messages_ja.properties

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ JobCopyBuilder.JobName.empty=\u5024\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3
3636
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
3737
# JobCopyBuilder.JobName.notExists=指定のジョブは存在しません。実行時にコピー元のジョブが存在しない場合、ビルドに失敗します。
3838
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
39+
# JobCopyBuilder.JobName.notAbstractItem=指定されたアイテムはコピーできません (AbstractItem を実装していない)。
40+
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
3941
# ReplaceOperation.fromStr.empty=値を指定してください。置き換え前の文字列が空文字列の場合、ビルドに失敗します。
4042
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
4143
# ReplaceOperation.fromStr.enclosedWithBlank=文字列の前後に空白文字があります。前後の空白文字を含んで置き換え処理を行うので、意図した通りの動作をしない場合があります。

‎src/test/java/jp/ikedam/jenkins/plugins/jobcopy_builder/JobcopyBuilderJenkinsTest.java

+455-20
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.