View Javadoc

1   /*
2    * Copyright 2011 Vincent Behar
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.rundeck.api;
17  
18  import org.apache.commons.io.FileUtils;
19  import org.apache.commons.io.IOUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.dom4j.Document;
22  import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
23  import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
24  import org.rundeck.api.domain.*;
25  import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
26  import org.rundeck.api.generator.DeleteExecutionsGenerator;
27  import org.rundeck.api.generator.ProjectConfigGenerator;
28  import org.rundeck.api.generator.ProjectConfigPropertyGenerator;
29  import org.rundeck.api.generator.ProjectGenerator;
30  import org.rundeck.api.parser.*;
31  import org.rundeck.api.query.ExecutionQuery;
32  import org.rundeck.api.util.AssertUtil;
33  import org.rundeck.api.util.PagedResults;
34  import org.rundeck.api.util.ParametersUtil;
35  
36  import java.io.*;
37  import java.util.*;
38  import java.util.concurrent.TimeUnit;
39  
40  /**
41   * Rundeck API client.
42   * <p>
43   * There are three methods for authentication : login-based or token-based or session-based.
44   * Login authentication requires
45   * both a "login" and a "password". Token-based requires a "token" (also called "auth-token"). See the Rundeck
46   * documentation for generating such a token.</p>
47   * <p>
48   *     Session-based authentication allows re-use of a previous login session. See {@link #testAuth()}.
49   * </p>
50   * <br>
51   * Usage : <br>
52   * <code>
53   * <pre>
54   * // using login-based authentication :
55   * RundeckClient rundeck = RundeckClient.builder()
56   *                           .url("http://localhost:4440")
57   *                           .login("admin", "admin").build();
58   * // or for a token-based authentication :
59   * RundeckClient rundeck = RundeckClient.builder()
60   *                           .url("http://localhost:4440")
61   *                           .token("PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD").build();
62   *
63   * List&lt;RundeckProject&gt; projects = rundeck.getProjects();
64   *
65   * RundeckJob job = rundeck.findJob("my-project", "main-group/sub-group", "job-name");
66   * RundeckExecution execution = rundeck.triggerJob(job.getId(),
67   *                                                 new OptionsBuilder().addOption("version", "1.2.0").toProperties());
68   *
69   * List&lt;RundeckExecution&gt; runningExecutions = rundeck.getRunningExecutions("my-project");
70   *
71   * rundeck.exportJobsToFile("/tmp/jobs.xml", FileType.XML, "my-project");
72   * rundeck.importJobs("/tmp/jobs.xml", FileType.XML);
73   * </pre>
74   * </code>
75   *
76   * @author Vincent Behar
77   */
78  public class RundeckClient implements Serializable {
79  
80      private static final long serialVersionUID = 1L;
81      public static final String JOBS_IMPORT = "/jobs/import";
82      public static final String STORAGE_ROOT_PATH = "/storage/";
83      public static final String STORAGE_KEYS_PATH = "keys/";
84  
85      /**
86       * Supported version numbers
87       */
88      public static enum Version {
89          V5(5),
90          V6(6),
91          V7(7),
92          V8(8),
93          V9(9),
94          V10(10),
95          V11(11),
96          V12(12),
97          V13(13),
98          ;
99  
100         private int versionNumber;
101 
102         Version(final int i) {
103             versionNumber = i;
104         }
105 
106         public int getVersionNumber() {
107             return versionNumber;
108         }
109     }
110     /** Version of the API supported */
111     public static final transient int API_VERSION = Version.V13.getVersionNumber();
112 
113     private static final String API = "/api/";
114 
115     /** End-point of the API */
116     public static final transient String API_ENDPOINT = API + API_VERSION;
117 
118     /** Default value for the "pooling interval" used when running jobs/commands/scripts */
119     public static final transient long DEFAULT_POOLING_INTERVAL = 5;
120 
121     /** Default unit of the "pooling interval" used when running jobs/commands/scripts */
122     public static final TimeUnit DEFAULT_POOLING_UNIT = TimeUnit.SECONDS;
123 
124     /** URL of the Rundeck instance ("http://localhost:4440" target="alexandria_uri">http://localhost:4440", "http://rundeck.your-compagny.com/", etc) */
125     private final String url;
126 
127     private int apiVersion = API_VERSION;
128 
129     private String token;
130 
131     private String login;
132 
133     private String password;
134 
135     private String sessionID;
136     private boolean sslHostnameVerifyAllowAll = false;
137     private boolean sslCertificateTrustAllowSelfSigned = false;
138     private boolean systemProxyEnabled = false;
139 
140     void setToken(String token) {
141         this.token = token;
142     }
143 
144     void setLogin(String login) {
145         this.login = login;
146     }
147 
148     void setPassword(String password) {
149         this.password = password;
150     }
151 
152     void setSessionID(String sessionID) {
153         this.sessionID = sessionID;
154     }
155 
156     int getApiVersion() {
157         return (apiVersion > 0 ? apiVersion : API_VERSION);
158     }
159 
160     void setApiVersion(int apiVersion) {
161         this.apiVersion = (apiVersion > 0 ? apiVersion : API_VERSION);
162     }
163 
164     void setApiVersion(Version apiVersion) {
165         setApiVersion(apiVersion.getVersionNumber());
166     }
167 
168     String getApiEndpoint() {
169         return API + getApiVersion();
170     }
171 
172     boolean isSslHostnameVerifyAllowAll() {
173         return sslHostnameVerifyAllowAll;
174     }
175 
176     void setSslHostnameVerifyAllowAll(boolean sslHostnameVerifyAllowAll) {
177         this.sslHostnameVerifyAllowAll = sslHostnameVerifyAllowAll;
178     }
179 
180     boolean isSslCertificateTrustAllowSelfSigned() {
181         return sslCertificateTrustAllowSelfSigned;
182     }
183 
184     void setSslCertificateTrustAllowSelfSigned(boolean sslCertificateTrustAllowSelfSigned) {
185         this.sslCertificateTrustAllowSelfSigned = sslCertificateTrustAllowSelfSigned;
186     }
187     boolean isSystemProxyEnabled() {
188         return systemProxyEnabled;
189     }
190 
191     void setSystemProxyEnabled(boolean systemProxyEnabled) {
192         this.systemProxyEnabled = systemProxyEnabled;
193     }
194 
195 
196     /**
197      * Used by RundeckClientBuilder
198      */
199     RundeckClient(final String url) throws IllegalArgumentException {
200         AssertUtil.notBlank(url, "The Rundeck URL is mandatory !");
201         this.url=url;
202     }
203 
204     /**
205      * Create a builder for RundeckClient
206      */
207     public static RundeckClientBuilder builder() {
208         return new RundeckClientBuilder();
209     }
210 
211     /**
212      * Try to "ping" the Rundeck instance to see if it is alive
213      *
214      * @throws RundeckApiException if the ping fails
215      */
216     public void ping() throws RundeckApiException {
217         new ApiCall(this).ping();
218     }
219 
220     /**
221      * Test the authentication on the Rundeck instance.
222      *
223      * @return sessionID if doing username+password login and it succeeded
224      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
225      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
226      */
227     public String testAuth() throws RundeckApiLoginException, RundeckApiTokenException {
228         return (new ApiCall(this)).testAuth();
229     }
230 
231 
232     /*
233      * Projects
234      */
235 
236     private ProjectParser createProjectParser() {
237         return createProjectParser(null);
238     }
239 
240     private ProjectParser createProjectParser(final String xpath) {
241         return new ProjectParserV11(xpath);
242     }
243 
244     /**
245      * List all projects
246      *
247      * @return a {@link List} of {@link RundeckProject} : might be empty, but won't be null
248      * @throws RundeckApiException in case of error when calling the API
249      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
250      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
251      */
252     public List<RundeckProject> getProjects() throws RundeckApiException, RundeckApiLoginException,
253             RundeckApiTokenException {
254         return new ApiCall(this).get(new ApiPathBuilder("/projects"),
255                                      new ListParser<>(
256                                              createProjectParser(),
257                                              "/projects/project"
258                                      ));
259     }
260 
261     /**
262      * Get the definition of a single project, identified by the given name
263      *
264      * @param projectName name of the project - mandatory
265      * @return a {@link RundeckProject} instance - won't be null
266      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
267      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
268      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
269      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
270      */
271     public RundeckProject getProject(String projectName) throws RundeckApiException, RundeckApiLoginException,
272             RundeckApiTokenException, IllegalArgumentException {
273         AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !");
274         return new ApiCall(this).get(new ApiPathBuilder("/project/", projectName),
275                 createProjectParser(
276                         (getApiVersion() < Version.V11.getVersionNumber()
277                                 ? "/projects/project"
278                                 : "/project"
279                         )));
280     }
281 
282     /**
283      * Create a new project, and return the new definition
284      *
285      * @param projectName name of the project - mandatory
286      * @param configuration project configuration properties
287      *
288      * @return a {@link RundeckProject} instance - won't be null
289      *
290      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
291      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
292      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
293      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
294      */
295     public RundeckProject createProject(String projectName, Map<String, String> configuration) throws
296             RundeckApiException, RundeckApiLoginException,
297             RundeckApiTokenException, IllegalArgumentException {
298 
299         AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
300         return new ApiCall(this)
301                 .post(new ApiPathBuilder("/projects").xml(
302                         projectDocument(projectName, configuration)
303                 ), createProjectParser(
304                         (getApiVersion() < Version.V11.getVersionNumber()
305                                 ? "/projects/project"
306                                 : "/project"
307                         )));
308     }
309     /**
310      * Delete a project
311      *
312      * @param projectName name of the project - mandatory
313      *
314      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
315      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
316      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
317      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
318      */
319     public void deleteProject(String projectName) throws
320             RundeckApiException, RundeckApiLoginException,
321             RundeckApiTokenException, IllegalArgumentException {
322 
323         AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
324         new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName));
325     }
326     /**
327      * Convenience method to export the archive of a project to the specified file.
328      *
329      * @param projectName name of the project - mandatory
330      * @param out         file to write to
331      * @return number of bytes written to the stream
332      *
333      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
334      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
335      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
336      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
337      */
338     public int exportProject(final String projectName, final File out) throws
339             RundeckApiException, RundeckApiLoginException,
340             RundeckApiTokenException, IllegalArgumentException, IOException {
341         final FileOutputStream fileOutputStream = new FileOutputStream(out);
342         try {
343             return exportProject(projectName, fileOutputStream);
344         }finally {
345             fileOutputStream.close();
346         }
347     }
348     /**
349      * Export the archive of a project to the specified outputstream
350      *
351      * @param projectName name of the project - mandatory
352      * @return number of bytes written to the stream
353      *
354      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
355      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
356      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
357      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
358      */
359     public int exportProject(String projectName, OutputStream out) throws
360             RundeckApiException, RundeckApiLoginException,
361             RundeckApiTokenException, IllegalArgumentException, IOException {
362 
363         AssertUtil.notBlank(projectName, "projectName is mandatory to export a project archive!");
364         return new ApiCall(this).get(
365                 new ApiPathBuilder("/project/", projectName, "/export")
366                         .accept("application/zip"),
367                 out);
368     }
369 
370     /**
371      * Import a archive file to the specified project.
372      *
373      * @param projectName name of the project - mandatory
374      * @param archiveFile zip archive file
375      * @param includeExecutions if true, import executions defined in the archive, otherwise skip them
376      * @param preserveJobUuids if true, do not remove UUIDs from imported jobs, otherwise remove them
377      *
378      * @return Result of the import request, may contain a list of import error messages
379      *
380      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
381      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
382      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
383      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
384      */
385     public ArchiveImport importArchive(final String projectName, final File archiveFile,
386             final boolean includeExecutions, final boolean preserveJobUuids) throws
387             RundeckApiException, RundeckApiLoginException,
388             RundeckApiTokenException, IllegalArgumentException, IOException {
389 
390         AssertUtil.notBlank(projectName, "projectName is mandatory to import a project archive!");
391         AssertUtil.notNull(archiveFile, "archiveFile is mandatory to import a project archive!"); ;
392         return callImportProject(projectName, includeExecutions, preserveJobUuids,
393                 new ApiPathBuilder().content("application/zip", archiveFile));
394     }
395 
396     private ArchiveImport callImportProject(final String projectName, final boolean includeExecutions, final boolean preserveJobUuids,
397             final ApiPathBuilder param) {
398         param.paths("/project/", projectName, "/import")
399         .param("importExecutions", includeExecutions)
400         .param("jobUuidOption", preserveJobUuids ? "preserve" : "remove");
401         return new ApiCall(this).put(
402                 param,
403                 new ArchiveImportParser()
404         );
405     }
406 
407     /**
408      * Return the configuration of a project
409      *
410      * @param projectName name of the project - mandatory
411      *
412      * @return a {@link ProjectConfig} instance - won't be null
413      *
414      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
415      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
416      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
417      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
418      */
419     public ProjectConfig getProjectConfig(String projectName) throws
420             RundeckApiException, RundeckApiLoginException,
421             RundeckApiTokenException, IllegalArgumentException {
422 
423         AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
424         return new ApiCall(this)
425                 .get(new ApiPathBuilder("/project/", projectName, "/config"), new ProjectConfigParser("/config"));
426     }
427     /**
428      * Get a single project configuration key
429      *
430      * @param projectName name of the project - mandatory
431      * @param key name of the configuration key
432      *
433      * @return value, or null if the value is not set
434      *
435      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
436      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
437      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
438      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
439      */
440     public String getProjectConfig(final String projectName, final String key) throws
441             RundeckApiException, RundeckApiLoginException,
442             RundeckApiTokenException, IllegalArgumentException {
443 
444         AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
445         AssertUtil.notBlank(key, "key is mandatory to get the config key value!");
446 
447         ConfigProperty configProperty = null;
448         try {
449             configProperty = new ApiCall(this)
450                     .get(new ApiPathBuilder("/project/", projectName, "/config/", key),
451                             new ProjectConfigPropertyParser("/property"));
452         } catch (RundeckApiException.RundeckApiHttpStatusException e) {
453             if(404==e.getStatusCode()){
454                 return null;
455             }
456             throw e;
457         }
458         return configProperty.getValue();
459     }
460     /**
461      * Set a single project configuration property value
462      *
463      * @param projectName name of the project - mandatory
464      * @param key name of the configuration property
465      * @param value value of the property
466      *
467      * @return new value
468      *
469      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
470      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
471      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
472      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
473      */
474     public String setProjectConfig(final String projectName, final String key, final String value) throws
475             RundeckApiException, RundeckApiLoginException,
476             RundeckApiTokenException, IllegalArgumentException {
477 
478         AssertUtil.notBlank(projectName, "projectName is mandatory to set the config of a project !");
479         AssertUtil.notBlank(key, "key is mandatory to set the config key value!");
480         AssertUtil.notBlank(value, "value is mandatory to set the config key value!");
481 
482         final ConfigProperty configProperty = new ApiCall(this)
483                 .put(new ApiPathBuilder("/project/", projectName, "/config/", key)
484                         .xml(new ProjectConfigPropertyGenerator(new ConfigProperty(key, value))),
485                         new ProjectConfigPropertyParser("/property"));
486 
487         return configProperty.getValue();
488     }
489     /**
490      * Set a single project configuration property value
491      *
492      * @param projectName name of the project - mandatory
493      * @param key name of the configuration property
494      *
495      * @return new value
496      *
497      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
498      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
499      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
500      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
501      */
502     public void deleteProjectConfig(final String projectName, final String key) throws
503             RundeckApiException, RundeckApiLoginException,
504             RundeckApiTokenException, IllegalArgumentException {
505 
506         AssertUtil.notBlank(projectName, "projectName is mandatory to set the config of a project !");
507         AssertUtil.notBlank(key, "key is mandatory to set the config key value!");
508 
509         new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName, "/config/",
510                 key).accept("application/xml"));
511     }
512     /**
513      * Return the configuration of a project
514      *
515      * @param projectName name of the project - mandatory
516      *
517      * @return a {@link ProjectConfig} instance - won't be null
518      *
519      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
520      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
521      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
522      * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
523      */
524     public ProjectConfig setProjectConfig(String projectName, Map<String,String> configuration) throws
525             RundeckApiException, RundeckApiLoginException,
526             RundeckApiTokenException, IllegalArgumentException {
527 
528         AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
529         return new ApiCall(this)
530                 .put(new ApiPathBuilder("/project/", projectName, "/config")
531                         .xml(new ProjectConfigGenerator(new ProjectConfig(configuration)))
532                         , new ProjectConfigParser("/config"));
533     }
534 
535     private Document projectDocument(String projectName, Map<String, String> configuration) {
536         RundeckProject project = new RundeckProject();
537         project.setName(projectName);
538         if (null != configuration) {
539             project.setProjectConfig(new ProjectConfig(configuration));
540         }
541         return new ProjectGenerator(project).generateXmlDocument();
542     }
543 
544     /**
545      * Store contents to a project readme.md or motd.md
546      * @param projectName project name
547      * @param filename filename, must be readme.md or motd.md
548      * @param content content
549      */
550     public void storeProjectFile(final String projectName, final String filename, final String content){
551         AssertUtil.notBlank(projectName, "projectName is mandatory to get the file!");
552         AssertUtil.notBlank(filename, "filename is mandatory to get choose the file!");
553         AssertUtil.notBlank(content, "content is mandatory to set content!");
554         AssertUtil.inList("filename must be in the list: ", filename, "readme.md", "motd.md");
555         new ApiCall(this)
556                 .put(new ApiPathBuilder("/project/", projectName, "/", filename)
557                              .content( "text/plain; charset=utf-8", content)
558                               .accept("text/plain"),
559                      new ApiCall.PlainTextHandler()
560                 );
561     }
562 
563     /**
564      * Read contents of a project readme.md or motd.md if it exsts, or return null
565      * @param projectName  project name
566      * @param filename filename, must be readme.md or motd.md
567      * @return contents, or null
568      */
569     public String readProjectFile(final String projectName, final String filename){
570         AssertUtil.notBlank(projectName, "projectName is mandatory to get the readme file!");
571         AssertUtil.notBlank(filename, "filename is mandatory to get choose the readme file!");
572         AssertUtil.inList("filename must be in the list: ", filename, "readme.md", "motd.md");
573         try {
574             return new ApiCall(this)
575                     .get(
576                             new ApiPathBuilder("/project/", projectName, "/", filename)
577                                     .accept("text/plain"),
578                             new ApiCall.PlainTextHandler()
579                     );
580         } catch (RundeckApiException.RundeckApiHttpStatusException e) {
581             if (e.getStatusCode() == 404) {
582                 return null;
583             }
584             throw e;
585         }
586     }
587 
588     /**
589      * Delete a project readme.md or motd.md
590      * @param projectName project name
591      * @param filename filename, must be readme.md or motd.md
592      */
593     public void deleteProjectFile(final String projectName, final String filename){
594         AssertUtil.notBlank(projectName, "projectName is mandatory to get the readme file!");
595         AssertUtil.notBlank(filename, "filename is mandatory to get choose the readme file!");
596         AssertUtil.inList("filename must be in the list: ", filename, "readme.md", "motd.md");
597         new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName, "/", filename));
598     }
599     /*
600      * Jobs
601      */
602 
603     /**
604      * List all jobs (for all projects)
605      *
606      * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
607      * @throws RundeckApiException in case of error when calling the API
608      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
609      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
610      */
611     public List<RundeckJob> getJobs() throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException {
612         List<RundeckJob> jobs = new ArrayList<RundeckJob>();
613         for (RundeckProject project : getProjects()) {
614             jobs.addAll(getJobs(project.getName()));
615         }
616         return jobs;
617     }
618 
619     /**
620      * List all jobs that belongs to the given project
621      *
622      * @param project name of the project - mandatory
623      * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
624      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
625      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
626      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
627      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
628      * @see #getJobs(String, String, String, String...)
629      */
630     public List<RundeckJob> getJobs(String project) throws RundeckApiException, RundeckApiLoginException,
631             RundeckApiTokenException, IllegalArgumentException {
632         return getJobs(project, null, null, new String[0]);
633     }
634 
635     /**
636      * List the jobs that belongs to the given project, and matches the given criteria (jobFilter, groupPath and jobIds)
637      *
638      * @param project name of the project - mandatory
639      * @param jobFilter a filter for the job Name - optional
640      * @param groupPath a group or partial group path to include all jobs within that group path - optional
641      * @param jobIds a list of Job IDs to include - optional
642      * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null
643      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
644      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
645      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
646      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
647      * @see #getJobs(String)
648      */
649     public List<RundeckJob> getJobs(String project, String jobFilter, String groupPath, String... jobIds)
650             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
651         AssertUtil.notBlank(project, "project is mandatory to get all jobs !");
652         return new ApiCall(this).get(new ApiPathBuilder("/jobs").param("project", project)
653                                                                 .param("jobFilter", jobFilter)
654                                                                 .param("groupPath", groupPath)
655                                                                 .param("idlist", StringUtils.join(jobIds, ",")),
656                                      new ListParser<RundeckJob>(new JobParser(), "/jobs/job"));
657     }
658 
659     /**
660      * Export the definitions of all jobs that belongs to the given project
661      *
662      * @param filename path of the file where the content should be saved - mandatory
663      * @param format of the export. See {@link FileType} - mandatory
664      * @param project name of the project - mandatory
665      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
666      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
667      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
668      * @throws IllegalArgumentException if the format or project is blank (null, empty or whitespace), or the format is
669      *             invalid
670      * @throws IOException if we failed to write to the file
671      * @see #exportJobsToFile(String, FileType, String, String, String, String...)
672      * @see #exportJobs(String, String)
673      */
674     public void exportJobsToFile(String filename, String format, String project) throws RundeckApiException,
675             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException {
676         AssertUtil.notBlank(format, "format is mandatory to export jobs !");
677         exportJobsToFile(filename, FileType.valueOf(StringUtils.upperCase(format)), project);
678     }
679 
680     /**
681      * Export the definitions of all jobs that belongs to the given project
682      *
683      * @param filename path of the file where the content should be saved - mandatory
684      * @param format of the export. See {@link FileType} - mandatory
685      * @param project name of the project - mandatory
686      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
687      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
688      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
689      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the format is null
690      * @throws IOException if we failed to write to the file
691      * @see #exportJobsToFile(String, FileType, String, String, String, String...)
692      * @see #exportJobs(FileType, String)
693      */
694     public void exportJobsToFile(String filename, FileType format, String project) throws RundeckApiException,
695             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException {
696         exportJobsToFile(filename, format, project, null, null, new String[0]);
697     }
698 
699     /**
700      * Export the definitions of the jobs that belongs to the given project, and matches the given criteria (jobFilter,
701      * groupPath and jobIds)
702      *
703      * @param filename path of the file where the content should be saved - mandatory
704      * @param format of the export. See {@link FileType} - mandatory
705      * @param project name of the project - mandatory
706      * @param jobFilter a filter for the job Name - optional
707      * @param groupPath a group or partial group path to include all jobs within that group path - optional
708      * @param jobIds a list of Job IDs to include - optional
709      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
710      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
711      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
712      * @throws IllegalArgumentException if the filename, format or project is blank (null, empty or whitespace), or the
713      *             format is invalid
714      * @throws IOException if we failed to write to the file
715      * @see #exportJobsToFile(String, FileType, String, String, String, String...)
716      * @see #exportJobs(FileType, String, String, String, String...)
717      */
718     public void exportJobsToFile(String filename, String format, String project, String jobFilter, String groupPath,
719             String... jobIds) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException,
720             IllegalArgumentException, IOException {
721         AssertUtil.notBlank(format, "format is mandatory to export jobs !");
722         exportJobsToFile(filename,
723                          FileType.valueOf(StringUtils.upperCase(format)),
724                          project,
725                          jobFilter,
726                          groupPath,
727                          jobIds);
728     }
729 
730     /**
731      * Export the definitions of the jobs that belongs to the given project, and matches the given criteria (jobFilter,
732      * groupPath and jobIds)
733      *
734      * @param filename path of the file where the content should be saved - mandatory
735      * @param format of the export. See {@link FileType} - mandatory
736      * @param project name of the project - mandatory
737      * @param jobFilter a filter for the job Name - optional
738      * @param groupPath a group or partial group path to include all jobs within that group path - optional
739      * @param jobIds a list of Job IDs to include - optional
740      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
741      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
742      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
743      * @throws IllegalArgumentException if the filename or project is blank (null, empty or whitespace), or the format
744      *             is null
745      * @throws IOException if we failed to write to the file
746      * @see #exportJobs(FileType, String, String, String, String...)
747      */
748     public void exportJobsToFile(String filename, FileType format, String project, String jobFilter, String groupPath,
749             String... jobIds) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException,
750             IllegalArgumentException, IOException {
751         AssertUtil.notBlank(filename, "filename is mandatory to export a job !");
752         InputStream inputStream = exportJobs(format, project, jobFilter, groupPath, jobIds);
753         FileUtils.writeByteArrayToFile(new File(filename), IOUtils.toByteArray(inputStream));
754     }
755 
756     /**
757      * Export the definitions of all jobs that belongs to the given project
758      *
759      * @param format of the export. See {@link FileType} - mandatory
760      * @param project name of the project - mandatory
761      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
762      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
763      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
764      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
765      * @throws IllegalArgumentException if the format or project is blank (null, empty or whitespace), or the format is
766      *             invalid
767      * @see #exportJobs(FileType, String, String, String, String...)
768      * @see #exportJobsToFile(String, String, String)
769      */
770     public InputStream exportJobs(String format, String project) throws RundeckApiException, RundeckApiLoginException,
771             RundeckApiTokenException, IllegalArgumentException {
772         AssertUtil.notBlank(format, "format is mandatory to export jobs !");
773         return exportJobs(FileType.valueOf(StringUtils.upperCase(format)), project);
774     }
775 
776     /**
777      * Export the definitions of all jobs that belongs to the given project
778      *
779      * @param format of the export. See {@link FileType} - mandatory
780      * @param project name of the project - mandatory
781      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
782      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
783      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
784      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
785      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the format is null
786      * @see #exportJobs(FileType, String, String, String, String...)
787      * @see #exportJobsToFile(String, FileType, String)
788      */
789     public InputStream exportJobs(FileType format, String project) throws RundeckApiException,
790             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
791         return exportJobs(format, project, null, null, new String[0]);
792     }
793 
794     /**
795      * Export the definitions of the jobs that belongs to the given project, and matches the given criteria (jobFilter,
796      * groupPath and jobIds)
797      *
798      * @param format of the export. See {@link FileType} - mandatory
799      * @param project name of the project - mandatory
800      * @param jobFilter a filter for the job Name - optional
801      * @param groupPath a group or partial group path to include all jobs within that group path - optional
802      * @param jobIds a list of Job IDs to include - optional
803      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
804      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
805      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
806      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
807      * @throws IllegalArgumentException if the format or project is blank (null, empty or whitespace), or the format is
808      *             invalid
809      * @see #exportJobs(FileType, String, String, String, String...)
810      * @see #exportJobsToFile(String, String, String, String, String, String...)
811      */
812     public InputStream exportJobs(String format, String project, String jobFilter, String groupPath, String... jobIds)
813             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
814         AssertUtil.notBlank(format, "format is mandatory to export jobs !");
815         return exportJobs(FileType.valueOf(StringUtils.upperCase(format)), project, jobFilter, groupPath, jobIds);
816     }
817 
818     /**
819      * Export the definitions of the jobs that belongs to the given project, and matches the given criteria (jobFilter,
820      * groupPath and jobIds)
821      *
822      * @param format of the export. See {@link FileType} - mandatory
823      * @param project name of the project - mandatory
824      * @param jobFilter a filter for the job Name - optional
825      * @param groupPath a group or partial group path to include all jobs within that group path - optional
826      * @param jobIds a list of Job IDs to include - optional
827      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
828      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
829      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
830      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
831      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the format is null
832      * @see #exportJobsToFile(String, FileType, String, String, String, String...)
833      */
834     public InputStream exportJobs(FileType format, String project, String jobFilter, String groupPath, String... jobIds)
835             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
836         AssertUtil.notNull(format, "format is mandatory to export jobs !");
837         AssertUtil.notBlank(project, "project is mandatory to export jobs !");
838         return new ApiCall(this).get(new ApiPathBuilder("/jobs/export")
839                 .accept(format == FileType.XML ? "text/xml" : "text/yaml")
840                 .param("format", format)
841                 .param("project", project)
842                 .param("jobFilter", jobFilter)
843                 .param("groupPath", groupPath)
844                 .param("idlist", StringUtils.join(jobIds, ",")),false);
845     }
846 
847     /**
848      * Export the definition of a single job (identified by the given ID)
849      *
850      * @param filename path of the file where the content should be saved - mandatory
851      * @param format of the export. See {@link FileType} - mandatory
852      * @param jobId identifier of the job - mandatory
853      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
854      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
855      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
856      * @throws IllegalArgumentException if the filename, format or jobId is blank (null, empty or whitespace), or the
857      *             format is invalid
858      * @throws IOException if we failed to write to the file
859      * @see #exportJobToFile(String, FileType, String)
860      * @see #exportJob(String, String)
861      * @see #getJob(String)
862      */
863     public void exportJobToFile(String filename, String format, String jobId) throws RundeckApiException,
864             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException {
865         AssertUtil.notBlank(format, "format is mandatory to export a job !");
866         exportJobToFile(filename, FileType.valueOf(StringUtils.upperCase(format)), jobId);
867     }
868 
869     /**
870      * Export the definition of a single job (identified by the given ID)
871      *
872      * @param filename path of the file where the content should be saved - mandatory
873      * @param format of the export. See {@link FileType} - mandatory
874      * @param jobId identifier of the job - mandatory
875      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
876      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
877      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
878      * @throws IllegalArgumentException if the filename or jobId is blank (null, empty or whitespace), or the format is
879      *             null
880      * @throws IOException if we failed to write to the file
881      * @see #exportJob(FileType, String)
882      * @see #getJob(String)
883      */
884     public void exportJobToFile(String filename, FileType format, String jobId) throws RundeckApiException,
885             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException {
886         AssertUtil.notBlank(filename, "filename is mandatory to export a job !");
887         InputStream inputStream = exportJob(format, jobId);
888         FileUtils.writeByteArrayToFile(new File(filename), IOUtils.toByteArray(inputStream));
889     }
890 
891     /**
892      * Export the definition of a single job, identified by the given ID
893      *
894      * @param format of the export. See {@link FileType} - mandatory
895      * @param jobId identifier of the job - mandatory
896      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
897      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
898      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
899      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
900      * @throws IllegalArgumentException if the format or jobId is blank (null, empty or whitespace), or the format is
901      *             invalid
902      * @see #exportJobToFile(String, String, String)
903      * @see #getJob(String)
904      */
905     public InputStream exportJob(String format, String jobId) throws RundeckApiException, RundeckApiLoginException,
906             RundeckApiTokenException, IllegalArgumentException {
907         AssertUtil.notBlank(format, "format is mandatory to export a job !");
908         return exportJob(FileType.valueOf(StringUtils.upperCase(format)), jobId);
909     }
910 
911     /**
912      * Export the definition of a single job, identified by the given ID
913      *
914      * @param format of the export. See {@link FileType} - mandatory
915      * @param jobId identifier of the job - mandatory
916      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
917      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
918      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
919      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
920      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace), or the format is null
921      * @see #exportJobToFile(String, FileType, String)
922      * @see #getJob(String)
923      */
924     public InputStream exportJob(FileType format, String jobId) throws RundeckApiException, RundeckApiLoginException,
925             RundeckApiTokenException, IllegalArgumentException {
926         AssertUtil.notNull(format, "format is mandatory to export a job !");
927         AssertUtil.notBlank(jobId, "jobId is mandatory to export a job !");
928         return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId).param("format", format), false);
929     }
930 
931 
932     /**
933      * Import the definitions of jobs, from the given input stream, using the given behavior
934      *
935      * @param rundeckJobsImport import request, see {@link RundeckJobsImportBuilder}
936      *
937      * @return a {@link RundeckJobsImportResult} instance - won't be null
938      *
939      * @throws RundeckApiException      in case of error when calling the API
940      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
941      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
942      * @throws IllegalArgumentException if the stream or fileType is null
943      * @see #importJobs(RundeckJobsImport)
944      */
945     public RundeckJobsImportResult importJobs(final String filename,final RundeckJobsImport rundeckJobsImport) throws RundeckApiException,
946             RundeckApiLoginException,
947             RundeckApiTokenException, IllegalArgumentException, IOException {
948         AssertUtil.notBlank(filename, "filename (of jobs file) is mandatory to import jobs !");
949         FileInputStream stream = null;
950         try {
951             stream = FileUtils.openInputStream(new File(filename));
952             return importJobs(RundeckJobsImportBuilder.builder(rundeckJobsImport).setStream(stream).build());
953         } finally {
954 
955             IOUtils.closeQuietly(stream);
956         }
957     }
958     /**
959      * Import the definitions of jobs, from the given input stream, using the given behavior
960      *
961      * @param rundeckJobsImport import request, see {@link RundeckJobsImportBuilder}
962      *
963      * @return a {@link RundeckJobsImportResult} instance - won't be null
964      *
965      * @throws RundeckApiException      in case of error when calling the API
966      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
967      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
968      * @throws IllegalArgumentException if the stream or fileType is null
969      * @see #importJobs(String, RundeckJobsImport)
970      */
971     public RundeckJobsImportResult importJobs(final RundeckJobsImport rundeckJobsImport) throws RundeckApiException,
972             RundeckApiLoginException,
973             RundeckApiTokenException, IllegalArgumentException {
974 
975         AssertUtil.notNull(rundeckJobsImport.getStream(), "inputStream of jobs is mandatory to import jobs !");
976         AssertUtil.notNull(rundeckJobsImport.getFileType(), "fileType is mandatory to import jobs !");
977         final ApiPathBuilder request = new ApiPathBuilder(JOBS_IMPORT)
978                 .param("format", rundeckJobsImport.getFileType())
979                 .param("dupeOption", rundeckJobsImport.getImportMethod())
980                 .attach("xmlBatch", rundeckJobsImport.getStream());
981         if(null!=rundeckJobsImport.getUuidImportBehavior()) {
982             //API v9
983             request.param("uuidOption", rundeckJobsImport.getUuidImportBehavior());
984         }
985         if(null!=rundeckJobsImport.getProject()) {
986             //API v8
987             request.param("project", rundeckJobsImport.getProject());
988         }
989         return new ApiCall(this).post(request, new JobsImportResultParser("result"));
990     }
991 
992     /**
993      * Find a job, identified by its project, group and name. Note that the groupPath is optional, as a job does not
994      * need to belong to a group (either pass null, or an empty string).
995      *
996      * @param project name of the project - mandatory
997      * @param groupPath group to which the job belongs (if it belongs to a group) - optional
998      * @param name of the job to find - mandatory
999      * @return a {@link RundeckJob} instance - null if not found
1000      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1001      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1002      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1003      * @throws IllegalArgumentException if the project or the name is blank (null, empty or whitespace)
1004      * @see #getJob(String)
1005      */
1006     public RundeckJob findJob(String project, String groupPath, String name) throws RundeckApiException,
1007             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1008         AssertUtil.notBlank(project, "project is mandatory to find a job !");
1009         AssertUtil.notBlank(name, "job name is mandatory to find a job !");
1010         List<RundeckJob> jobs = getJobs(project, name, groupPath, new String[0]);
1011         return jobs.isEmpty() ? null : jobs.get(0);
1012     }
1013 
1014     /**
1015      * Get the definition of a single job, identified by the given ID
1016      *
1017      * @param jobId identifier of the job - mandatory
1018      * @return a {@link RundeckJob} instance - won't be null
1019      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1020      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1021      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1022      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1023      * @see #findJob(String, String, String)
1024      * @see #exportJob(String, String)
1025      */
1026     public RundeckJob getJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
1027             RundeckApiTokenException, IllegalArgumentException {
1028         AssertUtil.notBlank(jobId, "jobId is mandatory to get the details of a job !");
1029         return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId), new JobParser("joblist/job"));
1030     }
1031 
1032     /**
1033      * Delete a single job, identified by the given ID
1034      *
1035      * @param jobId identifier of the job - mandatory
1036      * @return the success message (note that in case of error, you'll get an exception)
1037      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1038      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1039      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1040      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1041      */
1042     public String deleteJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
1043             RundeckApiTokenException, IllegalArgumentException {
1044         AssertUtil.notBlank(jobId, "jobId is mandatory to delete a job !");
1045         new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId));
1046         return "Job " + jobId + " was deleted successfully";
1047     }
1048     /**
1049      * Delete multiple jobs, identified by the given IDs
1050      *
1051      * @param jobIds List of job IDS
1052      * @return the bulk delete result
1053      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1054      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1055      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1056      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1057      */
1058     public RundeckJobDeleteBulk deleteJobs(final List<String> jobIds) throws RundeckApiException, RundeckApiLoginException,
1059             RundeckApiTokenException, IllegalArgumentException {
1060         if (null == jobIds || 0 == jobIds.size()) {
1061             throw new IllegalArgumentException("jobIds are mandatory to delete a job");
1062         }
1063         return new ApiCall(this).post(new ApiPathBuilder("/jobs/delete").field("ids",jobIds),
1064                                         new BulkDeleteParser("/deleteJobs"));
1065     }
1066 
1067     /**
1068      * Trigger the execution of a Rundeck job (identified by the given ID), and return immediately (without waiting the
1069      * end of the job execution)
1070      *
1071      * @param jobRun the RunJob, see {@link RunJobBuilder}
1072      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
1073      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1074      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1075      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1076      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1077      * @see #runJob(RunJob)
1078      */
1079     public RundeckExecution triggerJob(final RunJob jobRun)
1080             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1081         AssertUtil.notBlank(jobRun.getJobId(), "jobId is mandatory to trigger a job !");
1082         ApiPathBuilder apiPath = new ApiPathBuilder("/job/", jobRun.getJobId(), "/run").param("argString",
1083                 ParametersUtil.generateArgString(jobRun.getOptions()))
1084                 .nodeFilters(jobRun.getNodeFilters());
1085         if(null!=jobRun.getAsUser()) {
1086             apiPath.param("asUser", jobRun.getAsUser());
1087         }
1088         return new ApiCall(this).get(
1089                 apiPath,
1090                 new ExecutionParser(
1091                         "/executions/execution"
1092                 )
1093 
1094         );
1095     }
1096 
1097 
1098     /**
1099      * Run a Rundeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
1100      * We will poll the Rundeck server at regular interval (every 5 seconds) to know if the execution is finished (or
1101      * aborted) or is still running.
1102      *
1103      * @param runJob the RunJob, see {@link RunJobBuilder}
1104      *
1105      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1106      *
1107      * @throws RundeckApiException      in case of error when calling the API (non-existent job with this ID)
1108      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1109      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1110      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1111      * @see #triggerJob(RunJob)
1112      * @see #runJob(RunJob, long, TimeUnit)
1113      */
1114     public RundeckExecution runJob(final RunJob runJob) throws RundeckApiException, RundeckApiLoginException,
1115             RundeckApiTokenException, IllegalArgumentException {
1116         return runJob(runJob, DEFAULT_POOLING_INTERVAL, DEFAULT_POOLING_UNIT);
1117     }
1118 
1119     /**
1120      * Run a Rundeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return.
1121      * We will poll the Rundeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to
1122      * know if the execution is finished (or aborted) or is still running.
1123      *
1124      * @param jobRun the RunJob, see {@link RunJobBuilder}
1125      * @param poolingInterval for checking the status of the execution. Must be > 0.
1126      * @param poolingUnit     unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
1127      *
1128      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1129      *
1130      * @throws RundeckApiException      in case of error when calling the API (non-existent job with this ID)
1131      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1132      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1133      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1134      * @see #triggerJob(RunJob)
1135      */
1136     public RundeckExecution runJob(final RunJob jobRun, long poolingInterval,
1137             TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException,
1138             IllegalArgumentException {
1139 
1140         if (poolingInterval <= 0) {
1141             poolingInterval = DEFAULT_POOLING_INTERVAL;
1142             poolingUnit = DEFAULT_POOLING_UNIT;
1143         }
1144         if (poolingUnit == null) {
1145             poolingUnit = DEFAULT_POOLING_UNIT;
1146         }
1147 
1148         RundeckExecution execution = triggerJob(jobRun);
1149         while (ExecutionStatus.RUNNING.equals(execution.getStatus())) {
1150             try {
1151                 Thread.sleep(poolingUnit.toMillis(poolingInterval));
1152             } catch (InterruptedException e) {
1153                 break;
1154             }
1155             execution = getExecution(execution.getId());
1156         }
1157         return execution;
1158     }
1159 
1160     /*
1161      * Ad-hoc commands
1162      */
1163 
1164 
1165     /**
1166      * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution).
1167      * The command will be dispatched to nodes, accordingly to the nodeFilters parameter.
1168      *
1169      * @param command the RunAdhocCommand. Project and command are mandatory, see {@link RunAdhocCommandBuilder}
1170      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
1171      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1172      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1173      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1174      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
1175      * @see #runAdhocCommand(RunAdhocCommand)
1176      */
1177     public RundeckExecution triggerAdhocCommand(RunAdhocCommand command) throws RundeckApiException, RundeckApiLoginException,
1178             RundeckApiTokenException, IllegalArgumentException {
1179         AssertUtil.notBlank(command.getProject(), "project is mandatory to trigger an ad-hoc command !");
1180         AssertUtil.notBlank(command.getCommand(), "command is mandatory to trigger an ad-hoc command !");
1181         ApiPathBuilder apiPath = new ApiPathBuilder("/run/command").param("project", command.getProject())
1182                 .param("exec", command.getCommand())
1183                 .param("nodeThreadcount",
1184                         command.getNodeThreadcount())
1185                 .param("nodeKeepgoing",
1186                         command.getNodeKeepgoing())
1187                 .nodeFilters(command.getNodeFilters());
1188         if(null!= command.getAsUser()) {
1189             apiPath.param("asUser", command.getAsUser());
1190         }
1191         RundeckExecution execution = new ApiCall(this).get(apiPath, new ExecutionParser("/execution"));
1192         // the first call just returns the ID of the execution, so we need another call to get a "real" execution
1193         return getExecution(execution.getId());
1194     }
1195 
1196 
1197     /**
1198      * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the Rundeck
1199      * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still
1200      * running. The command will be dispatched to nodes, accordingly to the nodeFilters parameter.
1201      *
1202      * @param command the RunAdhocCommand, see {@link RunAdhocCommandBuilder}
1203      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1204      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1205      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1206      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1207      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
1208      * @see #runAdhocCommand(RunAdhocCommand, long, TimeUnit)
1209      * @see #triggerAdhocCommand(RunAdhocCommand)
1210      */
1211     public RundeckExecution runAdhocCommand(RunAdhocCommand command) throws RundeckApiException, RundeckApiLoginException,
1212             RundeckApiTokenException, IllegalArgumentException {
1213         return runAdhocCommand(command,
1214                                DEFAULT_POOLING_INTERVAL,
1215                                DEFAULT_POOLING_UNIT);
1216     }
1217 
1218     /**
1219      * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the Rundeck
1220      * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is
1221      * finished (or aborted) or is still running. The command will be dispatched to nodes, accordingly to the
1222      * nodeFilters parameter.
1223      *
1224      * @param command the RunAdhocCommand, see {@link RunAdhocCommandBuilder}
1225      * @param poolingInterval for checking the status of the execution. Must be > 0.
1226      * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
1227      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1228      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1229      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1230      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1231      * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace)
1232      * @see #triggerAdhocCommand(RunAdhocCommand)
1233      */
1234     public RundeckExecution runAdhocCommand(RunAdhocCommand command, long poolingInterval, TimeUnit poolingUnit)
1235             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1236         if (poolingInterval <= 0) {
1237             poolingInterval = DEFAULT_POOLING_INTERVAL;
1238             poolingUnit = DEFAULT_POOLING_UNIT;
1239         }
1240         if (poolingUnit == null) {
1241             poolingUnit = DEFAULT_POOLING_UNIT;
1242         }
1243 
1244         RundeckExecution execution = triggerAdhocCommand(command);
1245         while (ExecutionStatus.RUNNING.equals(execution.getStatus())) {
1246             try {
1247                 Thread.sleep(poolingUnit.toMillis(poolingInterval));
1248             } catch (InterruptedException e) {
1249                 break;
1250             }
1251             execution = getExecution(execution.getId());
1252         }
1253         return execution;
1254     }
1255 
1256     /*
1257      * Ad-hoc scripts
1258      */
1259 
1260 
1261     /**
1262      * Trigger the execution of an ad-hoc script read from a file, and return immediately (without waiting the end of
1263      * the execution). The script will be dispatched to nodes, accordingly to the nodeFilters parameter.
1264      *
1265      * @param script         the RunAdhocScript, see {@link RunAdhocScriptBuilder}
1266      * @param scriptFilename a file to read as the input script stream
1267      *
1268      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
1269      *
1270      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
1271      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1272      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1273      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null
1274      * @throws IOException              if an error occurs reading the script file
1275      * @see #triggerAdhocScript(RunAdhocScript)
1276      * @see #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)
1277      */
1278     public RundeckExecution triggerAdhocScript(final RunAdhocScript script, final String scriptFilename) throws
1279             RundeckApiException,
1280             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException {
1281         AssertUtil.notBlank(scriptFilename, "scriptFilename is mandatory to trigger an ad-hoc script !");
1282         FileInputStream stream = null;
1283         try {
1284             stream = FileUtils.openInputStream(new File(scriptFilename));
1285             return triggerAdhocScript(RunAdhocScriptBuilder.builder(script)
1286                     .setScript(stream)
1287                     .build());
1288         } finally {
1289             IOUtils.closeQuietly(stream);
1290         }
1291     }
1292     /**
1293      * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The
1294      * script will be dispatched to nodes, accordingly to the nodeFilters parameter.
1295      *
1296      * @param script the RunAdhocScript, see {@link RunAdhocScriptBuilder}
1297      * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null
1298      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1299      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1300      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1301      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null
1302      * @see #triggerAdhocScript(RunAdhocScript, String)
1303      * @see #runAdhocScript(RunAdhocScript)
1304      */
1305     public RundeckExecution triggerAdhocScript(RunAdhocScript script) throws RundeckApiException,
1306             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1307         AssertUtil.notBlank(script.getProject(), "project is mandatory to trigger an ad-hoc script !");
1308         AssertUtil.notNull(script.getScript(), "script is mandatory to trigger an ad-hoc script !");
1309         ApiPathBuilder apiPath = new ApiPathBuilder("/run/script").param("project", script.getProject())
1310                 .attach("scriptFile",
1311                         script.getScript())
1312                 .param("argString",script.getArgString())
1313                 .param("nodeThreadcount",
1314                         script.getNodeThreadcount())
1315                 .param("nodeKeepgoing",
1316                         script.getNodeKeepgoing())
1317                 .param("scriptInterpreter",
1318                         script.getScriptInterpreter())
1319                 .param("interpreterArgsQuoted",
1320                         script.getInterpreterArgsQuoted())
1321                 .nodeFilters(script.getNodeFilters());
1322         if(null!=script.getAsUser()) {
1323             apiPath.param("asUser", script.getAsUser());
1324         }
1325         RundeckExecution execution = new ApiCall(this).post(apiPath, new ExecutionParser("/execution"));
1326         // the first call just returns the ID of the execution, so we need another call to get a "real" execution
1327         return getExecution(execution.getId());
1328     }
1329 
1330 
1331     /**
1332      * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the Rundeck
1333      * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still
1334      * running. The script will be dispatched to nodes, accordingly to the nodeFilters parameter.
1335      *
1336      * @param script the RunAdhocScript, see {@link RunAdhocScriptBuilder}
1337      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1338      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1339      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1340      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1341      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null
1342      * @throws IOException if we failed to read the file
1343      * @see #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)
1344      * @see #triggerAdhocScript(RunAdhocScript)
1345      */
1346     public RundeckExecution runAdhocScript(RunAdhocScript script) throws RundeckApiException,
1347             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException {
1348         return runAdhocScript(script,
1349                               DEFAULT_POOLING_INTERVAL,
1350                               DEFAULT_POOLING_UNIT);
1351     }
1352 
1353     /**
1354      * Run an ad-hoc script read from a file, and wait until its execution is finished (or aborted) to return. We will
1355      * poll the Rundeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the
1356      * execution is finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to
1357      * the nodeFilters parameter.
1358      *
1359      * @param script          the RunAdhocScript, see {@link RunAdhocScriptBuilder}
1360      * @param scriptFilename  filename of a script to read
1361      * @param poolingInterval for checking the status of the execution. Must be > 0.
1362      * @param poolingUnit     unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
1363      *
1364      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1365      *
1366      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
1367      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1368      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1369      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null
1370      * @throws IOException              if we failed to read the file
1371      * @see #runAdhocScript(RunAdhocScript)
1372      * @see #triggerAdhocScript(RunAdhocScript, String)
1373      */
1374     public RundeckExecution runAdhocScript(final RunAdhocScript script, final String scriptFilename,
1375             final long poolingInterval, final TimeUnit poolingUnit) throws RundeckApiException,
1376             RundeckApiLoginException, RundeckApiTokenException,
1377             IllegalArgumentException, IOException {
1378         FileInputStream stream = null;
1379         try {
1380             stream = FileUtils.openInputStream(new File(scriptFilename));
1381             return runAdhocScript(RunAdhocScriptBuilder.builder(script)
1382                     .setScript(stream)
1383                     .build(), poolingInterval, poolingUnit);
1384         } finally {
1385             IOUtils.closeQuietly(stream);
1386         }
1387     }
1388     /**
1389      * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the Rundeck
1390      * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is
1391      * finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to the nodeFilters
1392      * parameter.
1393      *
1394      * @param script the RunAdhocScript, see {@link RunAdhocScriptBuilder}
1395      * @param poolingInterval for checking the status of the execution. Must be > 0.
1396      * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds.
1397      * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null
1398      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1399      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1400      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1401      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null
1402      * @throws IOException if we failed to read the file
1403      * @see #runAdhocScript(RunAdhocScript, long, TimeUnit)
1404      * @see #triggerAdhocScript(RunAdhocScript)
1405      */
1406     public RundeckExecution runAdhocScript(final RunAdhocScript script, long poolingInterval,
1407             TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException,
1408             IllegalArgumentException {
1409         if (poolingInterval <= 0) {
1410             poolingInterval = DEFAULT_POOLING_INTERVAL;
1411             poolingUnit = DEFAULT_POOLING_UNIT;
1412         }
1413         if (poolingUnit == null) {
1414             poolingUnit = DEFAULT_POOLING_UNIT;
1415         }
1416 
1417         RundeckExecution execution = triggerAdhocScript(script);
1418         while (ExecutionStatus.RUNNING.equals(execution.getStatus())) {
1419             try {
1420                 Thread.sleep(poolingUnit.toMillis(poolingInterval));
1421             } catch (InterruptedException e) {
1422                 break;
1423             }
1424             execution = getExecution(execution.getId());
1425         }
1426         return execution;
1427     }
1428 
1429     /*
1430      * Executions
1431      */
1432 
1433     /**
1434      * Get all running executions (for all projects)
1435      *
1436      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1437      * @throws RundeckApiException in case of error when calling the API
1438      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1439      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1440      * @see #getRunningExecutions(String)
1441      */
1442     public List<RundeckExecution> getRunningExecutions() throws RundeckApiException, RundeckApiLoginException,
1443             RundeckApiTokenException {
1444         if (this.getApiVersion() >= Version.V9.getVersionNumber()) {
1445             //simply query using '*'
1446             return getRunningExecutions("*");
1447         } else {
1448             List<RundeckExecution> executions = new ArrayList<RundeckExecution>();
1449             for (RundeckProject project : getProjects()) {
1450                 executions.addAll(getRunningExecutions(project.getName()));
1451             }
1452             return executions;
1453         }
1454     }
1455 
1456     /**
1457      * Get the running executions for the given project
1458      *
1459      * @param project name of the project - mandatory
1460      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1461      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1462      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1463      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1464      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1465      * @see #getRunningExecutions()
1466      */
1467     public List<RundeckExecution> getRunningExecutions(String project) throws RundeckApiException,
1468             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1469         AssertUtil.notBlank(project, "project is mandatory get all running executions !");
1470         return new ApiCall(this).get(new ApiPathBuilder("/executions/running").param("project", project),
1471                                      new ListParser<>(
1472                                              new ExecutionParser(),
1473                                              "/executions/execution"
1474                                      ));
1475     }
1476 
1477     /**
1478      * Get the executions of the given job
1479      *
1480      * @param jobId identifier of the job - mandatory
1481      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1482      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1483      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1484      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1485      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1486      * @see #getJobExecutions(String, RundeckExecution.ExecutionStatus, Long, Long)
1487      */
1488     public List<RundeckExecution> getJobExecutions(String jobId) throws RundeckApiException, RundeckApiLoginException,
1489             RundeckApiTokenException, IllegalArgumentException {
1490         return getJobExecutions(jobId, (ExecutionStatus) null);
1491     }
1492 
1493     /**
1494      * Get the executions of the given job
1495      *
1496      * @param jobId identifier of the job - mandatory
1497      * @param status of the executions, see {@link ExecutionStatus} - optional (null for all)
1498      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1499      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1500      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1501      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1502      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace), or the executionStatus is
1503      *             invalid
1504      * @see #getJobExecutions(String, String, Long, Long)
1505      */
1506     public List<RundeckExecution> getJobExecutions(String jobId, String status) throws RundeckApiException,
1507             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1508         return getJobExecutions(jobId,
1509                                 StringUtils.isBlank(status) ? null : ExecutionStatus.valueOf(StringUtils.upperCase(status)));
1510     }
1511 
1512     /**
1513      * Get the executions of the given job
1514      *
1515      * @param jobId identifier of the job - mandatory
1516      * @param status of the executions, see {@link ExecutionStatus} - optional (null for all)
1517      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1518      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1519      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1520      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1521      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1522      * @see #getJobExecutions(String, RundeckExecution.ExecutionStatus, Long, Long)
1523      */
1524     public List<RundeckExecution> getJobExecutions(String jobId, ExecutionStatus status) throws RundeckApiException,
1525             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1526         return getJobExecutions(jobId, status, null, null);
1527     }
1528 
1529     /**
1530      * Get the executions of the given job
1531      *
1532      * @param jobId identifier of the job - mandatory
1533      * @param status of the executions, see {@link ExecutionStatus} - optional (null for all)
1534      * @param max number of results to return - optional (null for all)
1535      * @param offset the 0-indexed offset for the first result to return - optional
1536      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1537      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1538      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1539      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1540      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace), or the executionStatus is
1541      *             invalid
1542      * @see #getJobExecutions(String, RundeckExecution.ExecutionStatus, Long, Long)
1543      */
1544     public List<RundeckExecution> getJobExecutions(String jobId, String status, Long max, Long offset)
1545             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1546         return getJobExecutions(jobId,
1547                                 StringUtils.isBlank(status) ? null : ExecutionStatus.valueOf(StringUtils.upperCase(status)),
1548                                 max,
1549                                 offset);
1550     }
1551 
1552     /**
1553      * Get the executions of the given job
1554      *
1555      * @param jobId identifier of the job - mandatory
1556      * @param status of the executions, see {@link ExecutionStatus} - optional (null for all)
1557      * @param max number of results to return - optional (null for all)
1558      * @param offset the 0-indexed offset for the first result to return - optional
1559      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1560      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1561      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1562      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1563      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1564      */
1565     public List<RundeckExecution> getJobExecutions(String jobId, ExecutionStatus status, Long max, Long offset)
1566             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1567         AssertUtil.notBlank(jobId, "jobId is mandatory to get the executions of a job !");
1568         return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId, "/executions").param("status", status)
1569                                          .param("max", max)
1570                                          .param("offset", offset),
1571                                      new ListParser<RundeckExecution>(new ExecutionParser(),
1572                                                                       "/executions/execution"));
1573     }
1574 
1575     /**
1576      * Get executions based on query parameters
1577      *
1578      * @param query query parameters for the request
1579      * @param max number of results to return - optional (null for all)
1580      * @param offset the 0-indexed offset for the first result to return - optional
1581      * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null
1582      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
1583      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1584      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1585      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
1586      */
1587     public PagedResults<RundeckExecution> getExecutions(ExecutionQuery query, Long max, Long offset)
1588         throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1589         if (!query.notBlank()) {
1590             throw new IllegalArgumentException("Some execution query parameter must be set");
1591         }
1592         AssertUtil.notBlank(query.getProject(), "project is required for execution query");
1593         return new ApiCall(this).get(
1594                 new ApiPathBuilder("/executions")
1595                         .param(new ExecutionQueryParameters(query))
1596                         .param("max", max)
1597                         .param("offset", offset),
1598                 new PagedResultParser<>(new ListParser<>(new ExecutionParser(), "execution"), "/executions")
1599         );
1600     }
1601 
1602     /**
1603      * Get a single execution, identified by the given ID
1604      *
1605      * @param executionId identifier of the execution - mandatory
1606      * @return a {@link RundeckExecution} instance - won't be null
1607      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
1608      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1609      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1610      * @throws IllegalArgumentException if the executionId is null
1611      */
1612     public RundeckExecution getExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException,
1613             RundeckApiTokenException, IllegalArgumentException {
1614         AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !");
1615         return new ApiCall(this).get(
1616                 new ApiPathBuilder("/execution/", executionId.toString()),
1617                 new ExecutionParser("/executions/execution")
1618         );
1619     }
1620 
1621     /**
1622      * Abort an execution (identified by the given ID). The execution should be running...
1623      *
1624      * @param executionId identifier of the execution - mandatory
1625      * @return a {@link RundeckAbort} instance - won't be null
1626      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
1627      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1628      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1629      * @throws IllegalArgumentException if the executionId is null
1630      */
1631     public RundeckAbort abortExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException,
1632             RundeckApiTokenException, IllegalArgumentException {
1633         return abortExecution(executionId, null);
1634     }
1635     /**
1636      * Abort an execution (identified by the given ID). The execution should be running...
1637      *
1638      * @param executionId identifier of the execution - mandatory
1639      * @param asUser specify a user name to abort the job as, must have 'killAs' permission
1640      * @return a {@link RundeckAbort} instance - won't be null
1641      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
1642      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1643      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1644      * @throws IllegalArgumentException if the executionId is null
1645      */
1646     public RundeckAbort abortExecution(Long executionId, final String asUser) throws RundeckApiException, RundeckApiLoginException,
1647             RundeckApiTokenException, IllegalArgumentException {
1648         AssertUtil.notNull(executionId, "executionId is mandatory to abort an execution !");
1649         ApiPathBuilder apiPath = new ApiPathBuilder("/execution/", executionId.toString(), "/abort");
1650         if(null!=asUser) {
1651             apiPath.param("asUser", asUser);
1652         }
1653         return new ApiCall(this).get(apiPath, new AbortParser( "/abort"));
1654     }
1655 
1656     /**
1657      * Delete all executions for a job specified by a job ID
1658      *
1659      * @param jobId Identifier for the job
1660      *
1661      * @return a {@link DeleteExecutionsResponse} instance - won't be null
1662      *
1663      * @throws RundeckApiException      in case of error when calling the API (non-existent
1664      *                                  execution with this ID)
1665      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1666      * @throws RundeckApiTokenException if the token is invalid (in case of token-based
1667      *                                  authentication)
1668      * @throws IllegalArgumentException if the executionIds is null
1669      */
1670     public DeleteExecutionsResponse deleteAllJobExecutions(final String jobId)
1671             throws RundeckApiException, RundeckApiLoginException,
1672                    RundeckApiTokenException, IllegalArgumentException
1673     {
1674         AssertUtil.notNull(jobId, "jobId is mandatory to delete executions!");
1675         return new ApiCall(this).delete(
1676                 new ApiPathBuilder("/job/",jobId,"/executions"),
1677                 new DeleteExecutionsResponseParser( "/deleteExecutions")
1678         );
1679     }
1680 
1681     /**
1682      * Delete a set of executions, identified by the given IDs
1683      *
1684      * @param executionIds set of identifiers for the executions - mandatory
1685      * @return a {@link DeleteExecutionsResponse} instance - won't be null
1686      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
1687      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1688      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1689      * @throws IllegalArgumentException if the executionIds is null
1690      */
1691     public DeleteExecutionsResponse deleteExecutions(final Set<Long> executionIds)
1692             throws RundeckApiException, RundeckApiLoginException,
1693                    RundeckApiTokenException, IllegalArgumentException
1694     {
1695         AssertUtil.notNull(executionIds, "executionIds is mandatory to delete executions!");
1696         if (executionIds.size() < 1) {
1697             throw new IllegalArgumentException("executionIds cannot be empty");
1698         }
1699         final ApiPathBuilder apiPath = new ApiPathBuilder("/executions/delete").xml(
1700                 new DeleteExecutionsGenerator(executionIds)
1701         );
1702         return new ApiCall(this).post(
1703                 apiPath,
1704                 new DeleteExecutionsResponseParser(  "/deleteExecutions")
1705         );
1706     }
1707 
1708     /**
1709      * Delete a single execution, identified by the given ID
1710      *
1711      * @param executionId identifier for the execution - mandatory
1712      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
1713      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1714      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1715      * @throws IllegalArgumentException if the executionId is null
1716      */
1717     public void deleteExecution(final Long executionId)
1718             throws RundeckApiException, RundeckApiLoginException,
1719                    RundeckApiTokenException, IllegalArgumentException
1720     {
1721         AssertUtil.notNull(executionId, "executionId is mandatory to delete an execution!");
1722         new ApiCall(this).delete(new ApiPathBuilder("/execution/", executionId.toString()));
1723     }
1724 
1725     /*
1726      * History
1727      */
1728 
1729     /**
1730      * Get the (events) history for the given project
1731      *
1732      * @param project name of the project - mandatory
1733      * @return a {@link RundeckHistory} instance - won't be null
1734      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1735      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1736      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1737      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1738      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1739      */
1740     public RundeckHistory getHistory(String project) throws RundeckApiException, RundeckApiLoginException,
1741             RundeckApiTokenException, IllegalArgumentException {
1742         return getHistory(project, null, null,(String) null, (String) null, null, null, null, null);
1743     }
1744 
1745     /**
1746      * Get the (events) history for the given project
1747      *
1748      * @param project name of the project - mandatory
1749      * @param max number of results to return - optional (default to 20)
1750      * @param offset the 0-indexed offset for the first result to return - optional (default to O)
1751      * @return a {@link RundeckHistory} instance - won't be null
1752      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1753      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1754      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1755      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1756      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1757      */
1758     public RundeckHistory getHistory(String project, Long max, Long offset) throws RundeckApiException,
1759             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1760         return getHistory(project, null, null, (String)null, (String)null, null, null, max, offset);
1761     }
1762 
1763     /**
1764      * Get the (events) history for the given project
1765      *
1766      * @param project name of the project - mandatory
1767      * @param jobId include only events matching the given job ID - optional
1768      * @param reportId include only events matching the given report ID - optional
1769      * @param user include only events created by the given user - optional
1770      * @return a {@link RundeckHistory} instance - won't be null
1771      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1772      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1773      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1774      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1775      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1776      */
1777     public RundeckHistory getHistory(String project, String jobId, String reportId, String user)
1778             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1779         return getHistory(project, jobId, reportId, user, null, null, null, null, null);
1780     }
1781 
1782     /**
1783      * Get the (events) history for the given project
1784      *
1785      * @param project name of the project - mandatory
1786      * @param jobId include only events matching the given job ID - optional
1787      * @param reportId include only events matching the given report ID - optional
1788      * @param user include only events created by the given user - optional
1789      * @param max number of results to return - optional (default to 20)
1790      * @param offset the 0-indexed offset for the first result to return - optional (default to O)
1791      * @return a {@link RundeckHistory} instance - won't be null
1792      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1793      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1794      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1795      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1796      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1797      */
1798     public RundeckHistory getHistory(String project, String jobId, String reportId, String user, Long max, Long offset)
1799             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1800         return getHistory(project, jobId, reportId, user, null, null, null, max, offset);
1801     }
1802 
1803     /**
1804      * Get the (events) history for the given project
1805      *
1806      * @param project name of the project - mandatory
1807      * @param recent include only events matching the given period of time. Format : "XY", where X is an integer, and Y
1808      *            is one of : "h" (hour), "d" (day), "w" (week), "m" (month), "y" (year). Example : "2w" (= last 2
1809      *            weeks), "5d" (= last 5 days), etc. Optional.
1810      * @return a {@link RundeckHistory} instance - won't be null
1811      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1812      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1813      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1814      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1815      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1816      */
1817     public RundeckHistory getHistory(String project, String recent) throws RundeckApiException,
1818             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1819         return getHistory(project, null, null, null, recent, null, null, null, null);
1820     }
1821 
1822     /**
1823      * Get the (events) history for the given project
1824      *
1825      * @param project name of the project - mandatory
1826      * @param recent include only events matching the given period of time. Format : "XY", where X is an integer, and Y
1827      *            is one of : "h" (hour), "d" (day), "w" (week), "m" (month), "y" (year). Example : "2w" (= last 2
1828      *            weeks), "5d" (= last 5 days), etc. Optional.
1829      * @param max number of results to return - optional (default to 20)
1830      * @param offset the 0-indexed offset for the first result to return - optional (default to O)
1831      * @return a {@link RundeckHistory} instance - won't be null
1832      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1833      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1834      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1835      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1836      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1837      */
1838     public RundeckHistory getHistory(String project, String recent, Long max, Long offset) throws RundeckApiException,
1839             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1840         return getHistory(project, null, null, null, recent, null, null, max, offset);
1841     }
1842 
1843     /**
1844      * Get the (events) history for the given project
1845      *
1846      * @param project name of the project - mandatory
1847      * @param begin date for the earlier events to retrieve - optional
1848      * @param end date for the latest events to retrieve - optional
1849      * @return a {@link RundeckHistory} instance - won't be null
1850      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1851      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1852      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1853      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1854      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1855      */
1856     public RundeckHistory getHistory(String project, Date begin, Date end) throws RundeckApiException,
1857             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1858         return getHistory(project, null, null, (String)null, (String)null, begin, end, null, null);
1859     }
1860 
1861     /**
1862      * Get the (events) history for the given project
1863      *
1864      * @param project name of the project - mandatory
1865      * @param begin date for the earlier events to retrieve - optional
1866      * @param end date for the latest events to retrieve - optional
1867      * @param max number of results to return - optional (default to 20)
1868      * @param offset the 0-indexed offset for the first result to return - optional (default to O)
1869      * @return a {@link RundeckHistory} instance - won't be null
1870      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1871      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1872      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1873      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1874      * @see #getHistory(String, String, String, String, String, Date, Date, Long, Long)
1875      */
1876     public RundeckHistory getHistory(String project, Date begin, Date end, Long max, Long offset)
1877             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1878         return getHistory(project, null, null, (String)null, (String) null, begin, end, max, offset);
1879     }
1880 
1881     /**
1882      * Get the (events) history for the given project
1883      *
1884      * @param project name of the project - mandatory
1885      * @param jobId include only events matching the given job ID - optional
1886      * @param reportId include only events matching the given report ID - optional
1887      * @param user include only events created by the given user - optional
1888      * @param recent include only events matching the given period of time. Format : "XY", where X is an integer, and Y
1889      *            is one of : "h" (hour), "d" (day), "w" (week), "m" (month), "y" (year). Example : "2w" (= last 2
1890      *            weeks), "5d" (= last 5 days), etc. Optional.
1891      * @param begin date for the earlier events to retrieve - optional
1892      * @param end date for the latest events to retrieve - optional
1893      * @param max number of results to return - optional (default to 20)
1894      * @param offset the 0-indexed offset for the first result to return - optional (default to O)
1895      * @return a {@link RundeckHistory} instance - won't be null
1896      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1897      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1898      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1899      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1900      */
1901     public RundeckHistory getHistory(String project, String jobId, String reportId, String user, String recent,
1902             Date begin, Date end, Long max, Long offset) throws RundeckApiException, RundeckApiLoginException,
1903             RundeckApiTokenException, IllegalArgumentException {
1904         AssertUtil.notBlank(project, "project is mandatory to get the history !");
1905         return new ApiCall(this).get(new ApiPathBuilder("/history").param("project", project)
1906                                          .param("jobIdFilter", jobId)
1907                                          .param("reportIdFilter", reportId)
1908                                          .param("userFilter", user)
1909                                          .param("recentFilter", recent)
1910                                          .param("begin", begin)
1911                                          .param("end", end)
1912                                          .param("max", max)
1913                                          .param("offset", offset),
1914                                      new HistoryParser("/events"));
1915     }
1916 
1917     /**
1918      * Get the (events) history for the given project
1919      *
1920      * @param project         name of the project - mandatory
1921      * @param includeJobNames list of job names ("group/name") to include results for
1922      * @param excludeJobNames list of job names ("group/name") to exclude results for
1923      * @param user            include only events created by the given user - optional
1924      * @param recent          include only events matching the given period of time. Format : "XY", where X is an
1925      *                        integer, and Y is one of : "h" (hour), "d" (day), "w" (week), "m" (month), "y" (year).
1926      *                        Example : "2w" (= last 2 weeks), "5d" (= last 5 days), etc. Optional.
1927      * @param begin           date for the earlier events to retrieve - optional
1928      * @param end             date for the latest events to retrieve - optional
1929      * @param max             number of results to return - optional (default to 20)
1930      * @param offset          the 0-indexed offset for the first result to return - optional (default to O)
1931      *
1932      * @return a {@link RundeckHistory} instance - won't be null
1933      *
1934      * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
1935      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1936      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1937      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1938      */
1939     public RundeckHistory getHistory(String project,
1940                                      String user,
1941                                      String recent,
1942                                      List<String> includeJobNames,
1943                                      List<String> excludeJobNames,
1944                                      Date begin,
1945                                      Date end,
1946                                      Long max,
1947                                      Long offset)
1948         throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
1949 
1950         AssertUtil.notBlank(project, "project is mandatory to get the history !");
1951         final ApiPathBuilder builder = new ApiPathBuilder("/history").param("project", project)
1952             .field("jobListFilter", includeJobNames)
1953             .field("excludeJobListFilter", excludeJobNames)
1954             .param("userFilter", user)
1955             .param("recentFilter", recent)
1956             .param("begin", begin)
1957             .param("end", end)
1958             .param("max", max)
1959             .param("offset", offset);
1960 
1961         return new ApiCall(this).postOrGet(builder, new HistoryParser("/events"));
1962     }
1963 
1964     /*
1965      * Nodes
1966      */
1967 
1968     /**
1969      * List all nodes (for all projects)
1970      *
1971      * @return a {@link List} of {@link RundeckNode} : might be empty, but won't be null
1972      * @throws RundeckApiException in case of error when calling the API
1973      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1974      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1975      */
1976     public List<RundeckNode> getNodes() throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException {
1977         List<RundeckNode> nodes = new ArrayList<RundeckNode>();
1978         for (RundeckProject project : getProjects()) {
1979             nodes.addAll(getNodes(project.getName()));
1980         }
1981         return nodes;
1982     }
1983 
1984     /**
1985      * List all nodes that belongs to the given project
1986      *
1987      * @param project name of the project - mandatory
1988      * @return a {@link List} of {@link RundeckNode} : might be empty, but won't be null
1989      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
1990      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
1991      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
1992      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
1993      * @see #getNodes(String, Properties)
1994      */
1995     public List<RundeckNode> getNodes(String project) throws RundeckApiException, RundeckApiLoginException,
1996             RundeckApiTokenException, IllegalArgumentException {
1997         return getNodes(project, null);
1998     }
1999 
2000     /**
2001      * List nodes that belongs to the given project
2002      *
2003      * @param project name of the project - mandatory
2004      * @param nodeFilters for filtering the nodes - optional. See {@link NodeFiltersBuilder}
2005      * @return a {@link List} of {@link RundeckNode} : might be empty, but won't be null
2006      * @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
2007      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2008      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2009      * @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
2010      */
2011     public List<RundeckNode> getNodes(String project, Properties nodeFilters) throws RundeckApiException,
2012             RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2013         AssertUtil.notBlank(project, "project is mandatory to get all nodes !");
2014         return new ApiCall(this).get(new ApiPathBuilder("/resources").param("project", project)
2015                                                                      .nodeFilters(nodeFilters),
2016                                      new ListParser<RundeckNode>(new NodeParser(), "project/node"));
2017     }
2018 
2019     /**
2020      * Get the definition of a single node
2021      *
2022      * @param name of the node - mandatory
2023      * @param project name of the project - mandatory
2024      * @return a {@link RundeckNode} instance - won't be null
2025      * @throws RundeckApiException in case of error when calling the API (non-existent name or project with this name)
2026      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2027      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2028      * @throws IllegalArgumentException if the name or project is blank (null, empty or whitespace)
2029      */
2030     public RundeckNode getNode(String name, String project) throws RundeckApiException, RundeckApiLoginException,
2031             RundeckApiTokenException, IllegalArgumentException {
2032         AssertUtil.notBlank(name, "the name of the node is mandatory to get a node !");
2033         AssertUtil.notBlank(project, "project is mandatory to get a node !");
2034         return new ApiCall(this).get(new ApiPathBuilder("/resource/", name).param("project", project),
2035                                      new NodeParser("project/node"));
2036     }
2037 
2038     /**
2039      * Get the output of a job execution
2040      *
2041      * @param executionId id of the execution - mandatory
2042      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
2043      * @throws RundeckApiException in case of error when calling the API (non-existent name or project with this name)
2044      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2045      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2046      * @throws IllegalArgumentException if the name or project is blank (null, empty or whitespace)
2047      */
2048     public InputStream getOutput(String executionId) throws RundeckApiException, RundeckApiLoginException,
2049             RundeckApiTokenException, IllegalArgumentException {
2050         AssertUtil.notBlank(executionId, "the execution id is mandatory to get execution output !");
2051         return new ApiCall(this).getNonApi(new ApiPathBuilder("/execution/downloadOutput/", executionId));
2052     }
2053 
2054     /**
2055      * Get the html page of the user's profile
2056      *
2057      * @param username - mandatory
2058      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
2059      * @throws RundeckApiException in case of error when calling the API (non-existent name or project with this name)
2060      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2061      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2062      * @throws IllegalArgumentException if the name or project is blank (null, empty or whitespace)
2063      */
2064     public InputStream getProfilePage(String username) throws RundeckApiException, RundeckApiLoginException,
2065             RundeckApiTokenException, IllegalArgumentException {
2066         AssertUtil.notBlank(username, "the username is mandatory to get profile page !");
2067         return new ApiCall(this).getNonApi(new ApiPathBuilder("/user/profile?login=", username));
2068     }
2069 
2070 
2071     /**
2072      * Generate a new token and get the result page (which is the html page of the user's profile)
2073      *
2074      * @param username - mandatory
2075      * @return an {@link InputStream} instance, not linked to any network resources - won't be null
2076      * @throws RundeckApiException in case of error when calling the API (non-existent name or project with this name)
2077      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2078      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2079      * @throws IllegalArgumentException if the name or project is blank (null, empty or whitespace)
2080      */
2081     public InputStream generateToken(String username) throws RundeckApiException, RundeckApiLoginException,
2082             RundeckApiTokenException, IllegalArgumentException {
2083         AssertUtil.notBlank(username, "the username is mandatory to generate the token");
2084         return new ApiCall(this).getNonApi(new ApiPathBuilder("/user/generateApiToken?login=", username));
2085     }
2086 
2087 
2088     /**
2089      * Get the execution output of the given job
2090      *
2091      * @param executionId identifier of the execution - mandatory
2092      * @param offset byte offset to read from in the file. 0 indicates the beginning.
2093      * @param lastlines nnumber of lines to retrieve from the end of the available output. If specified it will override the offset value and return only the specified number of lines at the end of the log.
2094      * @param lastmod epoch datestamp in milliseconds, return results only if modification changed since the specified date OR if more data is available at the given offset
2095      * @param maxlines maximum number of lines to retrieve forward from the specified offset.
2096      * @return {@link RundeckOutput}
2097      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
2098      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2099      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2100      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2101      */
2102     public RundeckOutput getExecutionOutput(Long executionId, int offset, int lastlines, long lastmod, int maxlines)
2103             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2104         AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!");
2105         ApiPathBuilder param = new ApiPathBuilder(
2106                 "/execution/", executionId.toString(),
2107                 "/output")
2108                 .param("offset", offset);
2109         if (lastlines > 0) {
2110             param.param("lastlines", lastlines);
2111         }
2112         if (lastmod >= 0) {
2113             param.param("lastmod", lastmod);
2114         }
2115         if (maxlines > 0) {
2116             param.param("maxlines", maxlines);
2117         }
2118         return new ApiCall(this).get(param,
2119                 new OutputParser("/output", createOutputEntryParser()));
2120     }
2121     /**
2122      * Get the execution state of the given execution
2123      *
2124      * @param executionId identifier of the execution - mandatory
2125      * @return {@link RundeckExecutionState} the execution state
2126      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
2127      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2128      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2129      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2130      */
2131     public RundeckExecutionState getExecutionState(Long executionId)
2132             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2133         AssertUtil.notNull(executionId, "executionId is mandatory to get the state of an execution!");
2134         ApiPathBuilder param = new ApiPathBuilder(
2135                 "/execution/", executionId.toString(),
2136                 "/state");
2137 
2138         return new ApiCall(this).get(param, new ExecutionStateParser("/executionState"));
2139     }
2140 
2141     /**
2142      * Get the execution output of the given execution on the specified node
2143      *
2144      * @param executionId identifier of the execution - mandatory
2145      * @param nodeName    name of the node
2146      * @param offset      byte offset to read from in the file. 0 indicates the beginning.
2147      * @param lastlines   nnumber of lines to retrieve from the end of the available output. If specified it will
2148      *                    override the offset value and return only the specified number of lines at the end of the
2149      *                    log.
2150      * @param lastmod     epoch datestamp in milliseconds, return results only if modification changed since the
2151      *                    specified date OR if more data is available at the given offset
2152      * @param maxlines    maximum number of lines to retrieve forward from the specified offset.
2153      *
2154      * @return {@link RundeckOutput}
2155      *
2156      * @throws RundeckApiException      in case of error when calling the API (non-existent job with this ID)
2157      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2158      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2159      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2160      */
2161     public RundeckOutput getExecutionOutputForNode(Long executionId, String nodeName, int offset, int lastlines,
2162             long lastmod, int maxlines)
2163             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2164         AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!");
2165         AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!");
2166         ApiPathBuilder param = new ApiPathBuilder(
2167                 "/execution/", executionId.toString(),
2168                 "/output/node/", nodeName)
2169                 .param("offset", offset);
2170         if(lastlines>0) {
2171             param.param("lastlines", lastlines);
2172         }
2173         if(lastmod>=0) {
2174             param.param("lastmod", lastmod);
2175         }
2176         if(maxlines>0) {
2177             param.param("maxlines", maxlines);
2178         }
2179         return new ApiCall(this).get(param,
2180                 new OutputParser("/output", createOutputEntryParser()));
2181     }
2182     /**
2183      * Get the execution output of the given execution for the specified step
2184      *
2185      * @param executionId identifier of the execution - mandatory
2186      * @param stepCtx     identifier for the step
2187      * @param offset      byte offset to read from in the file. 0 indicates the beginning.
2188      * @param lastlines   nnumber of lines to retrieve from the end of the available output. If specified it will
2189      *                    override the offset value and return only the specified number of lines at the end of the
2190      *                    log.
2191      * @param lastmod     epoch datestamp in milliseconds, return results only if modification changed since the
2192      *                    specified date OR if more data is available at the given offset
2193      * @param maxlines    maximum number of lines to retrieve forward from the specified offset.
2194      *
2195      * @return {@link RundeckOutput}
2196      *
2197      * @throws RundeckApiException      in case of error when calling the API (non-existent job with this ID)
2198      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2199      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2200      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2201      */
2202     public RundeckOutput getExecutionOutputForStep(Long executionId, String stepCtx, int offset, int lastlines,
2203             long lastmod, int maxlines)
2204             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2205         AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!");
2206         AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!");
2207         ApiPathBuilder param = new ApiPathBuilder(
2208                 "/execution/", executionId.toString(),
2209                 "/output/step/", stepCtx)
2210                 .param("offset", offset);
2211         if (lastlines > 0) {
2212             param.param("lastlines", lastlines);
2213         }
2214         if (lastmod >= 0) {
2215             param.param("lastmod", lastmod);
2216         }
2217         if (maxlines > 0) {
2218             param.param("maxlines", maxlines);
2219         }
2220         return new ApiCall(this).get(param,
2221                 new OutputParser("/output", createOutputEntryParser()));
2222     }
2223     /**
2224      * Get the execution output of the given execution for the specified step
2225      *
2226      * @param executionId identifier of the execution - mandatory
2227      * @param stepCtx     identifier for the step
2228      * @param offset      byte offset to read from in the file. 0 indicates the beginning.
2229      * @param lastlines   nnumber of lines to retrieve from the end of the available output. If specified it will
2230      *                    override the offset value and return only the specified number of lines at the end of the
2231      *                    log.
2232      * @param lastmod     epoch datestamp in milliseconds, return results only if modification changed since the
2233      *                    specified date OR if more data is available at the given offset
2234      * @param maxlines    maximum number of lines to retrieve forward from the specified offset.
2235      *
2236      * @return {@link RundeckOutput}
2237      *
2238      * @throws RundeckApiException      in case of error when calling the API (non-existent job with this ID)
2239      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2240      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2241      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2242      */
2243     public RundeckOutput getExecutionOutputForNodeAndStep(Long executionId, String nodeName, String stepCtx,
2244             int offset, int lastlines,
2245             long lastmod, int maxlines)
2246             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2247         AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!");
2248         AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!");
2249         AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!");
2250         ApiPathBuilder param = new ApiPathBuilder(
2251                 "/execution/", executionId.toString(),
2252                 "/output/node/", nodeName,
2253                 "/step/", stepCtx)
2254                 .param("offset", offset);
2255         if (lastlines > 0) {
2256             param.param("lastlines", lastlines);
2257         }
2258         if (lastmod >= 0) {
2259             param.param("lastmod", lastmod);
2260         }
2261         if (maxlines > 0) {
2262             param.param("maxlines", maxlines);
2263         }
2264         return new ApiCall(this).get(param,
2265                 new OutputParser("/output", createOutputEntryParser()));
2266     }
2267 
2268 
2269     /**
2270      * Get the execution output of the given job
2271      *
2272      * @param executionId identifier of the execution - mandatory
2273      * @param offset byte offset to read from in the file. 0 indicates the beginning.
2274      * @param lastmod epoch datestamp in milliseconds, return results only if modification changed since the specified date OR if more data is available at the given offset
2275      * @param maxlines maximum number of lines to retrieve forward from the specified offset.
2276      * @return {@link RundeckOutput}
2277      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
2278      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2279      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2280      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2281      */
2282     public RundeckOutput getExecutionOutput(Long executionId, int offset, long lastmod, int maxlines)
2283             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2284         AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!");
2285         ApiPathBuilder param = new ApiPathBuilder("/execution/", executionId.toString(), "/output")
2286                 .param("offset", offset);
2287         if (lastmod >= 0) {
2288             param.param("lastmod", lastmod);
2289         }
2290         if (maxlines > 0) {
2291             param.param("maxlines", maxlines);
2292         }
2293         return new ApiCall(this).get(param, new OutputParser("/output", createOutputEntryParser()));
2294     }
2295     /**
2296      * Get the execution state output sequence of the given job
2297      *
2298      * @param executionId identifier of the execution - mandatory
2299      * @param stateOnly if true, include only state change output entries, otherwise include state and log entries
2300      * @param offset byte offset to read from in the file. 0 indicates the beginning.
2301      * @param lastmod epoch datestamp in milliseconds, return results only if modification changed since the specified date OR if more data is available at the given offset
2302      * @param maxlines maximum number of lines to retrieve forward from the specified offset.
2303      * @return {@link RundeckOutput}
2304      * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID)
2305      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2306      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2307      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
2308      */
2309     public RundeckOutput getExecutionOutputState(Long executionId, boolean stateOnly, int offset, long lastmod,
2310             int maxlines)
2311             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
2312         AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!");
2313         ApiPathBuilder param = new ApiPathBuilder("/execution/", executionId.toString(), "/output/state")
2314                 .param("offset", offset);
2315         if (lastmod >= 0) {
2316             param.param("lastmod", lastmod);
2317         }
2318         if (maxlines > 0) {
2319             param.param("maxlines", maxlines);
2320         }
2321         if(stateOnly) {
2322             param.param("stateOnly", true);
2323         }
2324         return new ApiCall(this).get(param, new OutputParser("/output", createOutputEntryParser()));
2325     }
2326 
2327     private OutputEntryParser createOutputEntryParser() {
2328         if (getApiVersion() <= Version.V5.versionNumber) {
2329             return new OutputEntryParserV5();
2330         }else{
2331             return new OutputEntryParser();
2332         }
2333     }
2334 
2335     
2336     /*
2337      * System Info
2338      */
2339 
2340     /**
2341      * Get system informations about the Rundeck server
2342      *
2343      * @return a {@link RundeckSystemInfo} instance - won't be null
2344      * @throws RundeckApiException in case of error when calling the API
2345      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
2346      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
2347      */
2348     public RundeckSystemInfo getSystemInfo() throws RundeckApiException, RundeckApiLoginException,
2349             RundeckApiTokenException {
2350         return new ApiCall(this).get(new ApiPathBuilder("/system/info"), new SystemInfoParser("/system"));
2351     }
2352 
2353 
2354     /*
2355      * API token
2356      */
2357 
2358     /**
2359      * List API tokens for a user.
2360      * @param user username
2361      * @return list of tokens
2362      * @throws RundeckApiException
2363      */
2364     public List<RundeckToken> listApiTokens(final String user) throws RundeckApiException {
2365         AssertUtil.notNull(user, "user is mandatory to list API tokens for a user.");
2366         return new ApiCall(this).
2367                 get(new ApiPathBuilder("/tokens/", user),
2368                         new ListParser<RundeckToken>(new RundeckTokenParser(), "/tokens/token"));
2369     }
2370 
2371     /**
2372      * List all API tokens
2373      * @return list of tokens
2374      * @throws RundeckApiException
2375      */
2376     public List<RundeckToken> listApiTokens() throws RundeckApiException {
2377         return new ApiCall(this).
2378                 get(new ApiPathBuilder("/tokens"),
2379                         new ListParser<RundeckToken>(new RundeckTokenParser(), "/tokens/token"));
2380     }
2381 
2382     /**
2383      * Generate an API token for a user.
2384      * @param user
2385      * @return
2386      * @throws RundeckApiException
2387      */
2388     public String generateApiToken(final String user) throws RundeckApiException{
2389         AssertUtil.notNull(user, "user is mandatory to generate an API token for a user.");
2390         RundeckToken result = new ApiCall(this).
2391                 post(new ApiPathBuilder("/tokens/", user).emptyContent(),
2392                         new RundeckTokenParser("/token"));
2393         return result.getToken();
2394     }
2395     /**
2396      * Delete an existing token
2397      * @param token
2398      * @return
2399      * @throws RundeckApiException
2400      */
2401     public boolean deleteApiToken(final String token) throws RundeckApiException{
2402         AssertUtil.notNull(token, "token is mandatory to delete an API token.");
2403         new ApiCall(this).delete(new ApiPathBuilder("/token/", token));
2404         return true;
2405     }
2406     /**
2407      * Return user info for an existing token
2408      * @param token
2409      * @return token info
2410      * @throws RundeckApiException
2411      */
2412     public RundeckToken getApiToken(final String token) throws RundeckApiException{
2413         AssertUtil.notNull(token, "token is mandatory to get an API token.");
2414         return new ApiCall(this).get(new ApiPathBuilder("/token/", token), new RundeckTokenParser("/token"));
2415     }
2416 
2417     /**
2418      * Store an key file
2419      * @param path ssh key storage path, must start with "keys/"
2420      * @param keyfile key file
2421      * @param privateKey true to store private key, false to store public key
2422      * @return the key resource
2423      * @throws RundeckApiException
2424      */
2425     public KeyResource storeKey(final String path, final File keyfile, boolean privateKey) throws RundeckApiException{
2426         AssertUtil.notNull(path, "path is mandatory to store an key.");
2427         AssertUtil.notNull(keyfile, "keyfile is mandatory to store an key.");
2428         if (!path.startsWith(STORAGE_KEYS_PATH)) {
2429             throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
2430         }
2431         return new ApiCall(this).post(
2432                 new ApiPathBuilder(STORAGE_ROOT_PATH, path).content(
2433                         privateKey ? "application/octet-stream" : "application/pgp-keys",
2434                         keyfile
2435                 ),
2436                 new SSHKeyResourceParser("/resource")
2437         );
2438     }
2439 
2440     /**
2441      * Get metadata for an key file
2442      *
2443      * @param path ssh key storage path, must start with "keys/"
2444      *
2445      * @return the ssh key resource
2446      *
2447      * @throws RundeckApiException if there is an error, or if the path is a directory not a file
2448      */
2449     public KeyResource getKey(final String path) throws RundeckApiException {
2450         AssertUtil.notNull(path, "path is mandatory to get an key.");
2451         if (!path.startsWith(STORAGE_KEYS_PATH)) {
2452             throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
2453         }
2454         KeyResource storageResource = new ApiCall(this).get(
2455                 new ApiPathBuilder(STORAGE_ROOT_PATH, path),
2456                 new SSHKeyResourceParser("/resource")
2457         );
2458         if (storageResource.isDirectory()) {
2459             throw new RundeckApiException("Key Path is a directory: " + path);
2460         }
2461         return storageResource;
2462     }
2463 
2464     /**
2465      * Get content for a public key file
2466      * @param path ssh key storage path, must start with "keys/"
2467      * @param out outputstream to write data to
2468      *
2469      * @return length of written data
2470      * @throws RundeckApiException
2471      */
2472     public int getPublicKeyContent(final String path, final OutputStream out) throws
2473             RundeckApiException, IOException {
2474         AssertUtil.notNull(path, "path is mandatory to get an key.");
2475         if (!path.startsWith(STORAGE_KEYS_PATH)) {
2476             throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
2477         }
2478         try {
2479             return new ApiCall(this).get(
2480                     new ApiPathBuilder(STORAGE_ROOT_PATH, path)
2481                             .accept("application/pgp-keys")
2482                             .requireContentType("application/pgp-keys"),
2483                     out
2484             );
2485         } catch (RundeckApiException.RundeckApiHttpContentTypeException e) {
2486             throw new RundeckApiException("Requested Key path was not a Public key: " + path, e);
2487         }
2488     }
2489 
2490     /**
2491      * Get content for a public key file
2492      * @param path ssh key storage path, must start with "keys/"
2493      * @param out file to write data to
2494      * @return length of written data
2495      * @throws RundeckApiException
2496      */
2497     public int getPublicKeyContent(final String path, final File out) throws
2498             RundeckApiException, IOException {
2499         final FileOutputStream fileOutputStream = new FileOutputStream(out);
2500         try {
2501             return getPublicKeyContent(path, fileOutputStream);
2502         }finally {
2503             fileOutputStream.close();
2504         }
2505     }
2506 
2507     /**
2508      * List contents of root key directory
2509      *
2510      * @return list of key resources
2511      * @throws RundeckApiException
2512      */
2513     public List<KeyResource> listKeyDirectoryRoot() throws RundeckApiException {
2514         return listKeyDirectory(STORAGE_KEYS_PATH);
2515     }
2516     /**
2517      * List contents of key directory
2518      *
2519      * @param path ssh key storage path, must start with "keys/"
2520      *
2521      * @throws RundeckApiException if there is an error, or if the path is a file not a directory
2522      */
2523     public List<KeyResource> listKeyDirectory(final String path) throws RundeckApiException {
2524         AssertUtil.notNull(path, "path is mandatory to get an key.");
2525         if (!path.startsWith(STORAGE_KEYS_PATH)) {
2526             throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
2527         }
2528         KeyResource storageResource = new ApiCall(this).get(
2529                 new ApiPathBuilder(STORAGE_ROOT_PATH, path),
2530                 new SSHKeyResourceParser("/resource")
2531         );
2532         if(!storageResource.isDirectory()) {
2533             throw new RundeckApiException("key path is not a directory path: " + path);
2534         }
2535         return storageResource.getDirectoryContents();
2536     }
2537 
2538     /**
2539      * Delete an key file
2540      * @param path a path to a key file, must start with "keys/"
2541      */
2542     public void deleteKey(final String path){
2543         AssertUtil.notNull(path, "path is mandatory to delete an key.");
2544         if (!path.startsWith(STORAGE_KEYS_PATH)) {
2545             throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
2546         }
2547         new ApiCall(this).delete(new ApiPathBuilder(STORAGE_ROOT_PATH, path));
2548     }
2549 
2550     /**
2551      * @return the URL of the Rundeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc)
2552      */
2553     public String getUrl() {
2554         return url;
2555     }
2556 
2557     /**
2558      * @return the auth-token used for authentication on the Rundeck instance (null if using login-based or session-based auth)
2559      */
2560     public String getToken() {
2561         return token;
2562     }
2563 
2564     /**
2565      * @return the login used for authentication on the Rundeck instance (null if using token-based or session-based auth)
2566      */
2567     public String getLogin() {
2568         return login;
2569     }
2570 
2571     /**
2572      * @return the password used for authentication on the Rundeck instance (null if using token-based or session-based auth)
2573      */
2574     public String getPassword() {
2575         return password;
2576     }
2577 
2578     /**
2579      * @return the sessionID used for authentication on the Rundeck instance (null if using login-based or token-based auth)
2580      */
2581     public String getSessionID() {
2582         return sessionID;
2583     }
2584 
2585     @Override
2586     public String toString() {
2587         StringBuilder str = new StringBuilder();
2588         str.append("RundeckClient ").append(API_VERSION);
2589         str.append(" [").append(url).append("] ");
2590         if (token != null) {
2591             str.append("(token=").append(token).append(")");
2592         } else {
2593             str.append("(credentials=").append(login).append("|").append(password).append(")");
2594         }
2595         return str.toString();
2596     }
2597 
2598     @Override
2599     public int hashCode() {
2600         final int prime = 31;
2601         int result = 1;
2602         result = prime * result + ((login == null) ? 0 : login.hashCode());
2603         result = prime * result + ((password == null) ? 0 : password.hashCode());
2604         result = prime * result + ((token == null) ? 0 : token.hashCode());
2605         result = prime * result + ((url == null) ? 0 : url.hashCode());
2606         return result;
2607     }
2608 
2609     @Override
2610     public boolean equals(Object obj) {
2611         if (this == obj)
2612             return true;
2613         if (obj == null)
2614             return false;
2615         if (getClass() != obj.getClass())
2616             return false;
2617         RundeckClient other = (RundeckClient) obj;
2618         if (login == null) {
2619             if (other.login != null)
2620                 return false;
2621         } else if (!login.equals(other.login))
2622             return false;
2623         if (password == null) {
2624             if (other.password != null)
2625                 return false;
2626         } else if (!password.equals(other.password))
2627             return false;
2628         if (token == null) {
2629             if (other.token != null)
2630                 return false;
2631         } else if (!token.equals(other.token))
2632             return false;
2633         if (url == null) {
2634             if (other.url != null)
2635                 return false;
2636         } else if (!url.equals(other.url))
2637             return false;
2638         return true;
2639     }
2640 
2641 }